The React team released React 18 in May 2023, which came with better support for React Server Components (RSCs). Less than a year later, RedwoodJS announced support for server-side rendering and RSCs. This is a shift from Redwood’s traditional focus on GraphQL, but keeps the idea that it’s supposed to help you go from idea to startup as quick as you can.
In this article, we’ll look at what this change means to developing with Redwood, create a simple app to explore RSCs in the framework, and discuss some of the workarounds you’ll have to use while Bighorn is still in active development.
RedwoodJS was already easy to use. I have experimented with it before and was actually considering using it for my side projects, but at the time, it was in alpha. Now that it’s production-ready (though RSCs are not), I’m looking at it again — and I think RSCs will make it even better.
Despite being easy to use, when I first used the framework, I didn’t know GraphQL. Not that it was that hard to pick up, but there was some overhead to juggle that I know I have completely forgotten now.
If I needed to pick up GraphQL again, I would have to climb down the same rabbit hole to use again. With this new version, I won’t have to. While Redwood with RSCs is a canary version and not ready for production — and Redwood will continue to support GraphQL — it will be RSC-by-default in the future.
Although it takes some time to wrap your head around RSCs, you’re at least part of the way there when you know React.
RedwoodJS makes development easy. I have played with it before. But this time, I had some issues when I went to build and serve the application, so setup wasn’t as easy for me as it was the last time. Fortunately, I got it figured out, so you don’t have to run into the same issues.
With the current version of RedwoodJS I’m using, which is 8.0.0-canary.496
, you must use Node v20.10.0 or higher. I’m guessing any future version will require that, which I will explain after the install steps.
Once you have a compatible version of Node, run this command:
npx -y create-redwood-app@canary -y redwood_rsc_app
Do not forget this step (which I did initially), or the next few steps will greet you with error Command "rw" not found.
rw
is shorthand for redwood
. Here’s the next command:
yarn install
If you get an error when running the last command, run this:
corepack enable
To use React Server Components with RedwoodJS, you must install these experimental features:
yarn rw experimental setup-streaming-ssr -f yarn rw experimental setup-rsc
Then run the following commands to build and start the app:
yarn rw build yarn rw serve
This is the step where I ran into this error:
".../client-build-manifest.json" needs an import assertion of type "json"
It was because I was running Node v20.3.0. I actually tracked the issue down and edited files in my node_modules
folder to fix it, but after some research and experimentation, I found that in Node version 20.10.0, the following type of assert:
import data from './data.json' assert { type: 'json
Changed to this:
import data from './data.json' with { type: 'json
Here are more details on the issue I ran into. Upgrading to Node v20.10.0 and running yarn install
again fixed my issues without editing any files. Here is what you should ultimately get when visiting http://localhost:8910/:
This example application focuses on using React Server Components in Redwood. If you want to see an example that showcases more of the features of RedwoodJS, check out How to build a full-stack app in RedwoodJS.
To simplify the process and get to using RSCs quicker, I am going to use a project I just built as-is and put the memory game in the homepage. I’m going to use the free Cat as a service (Cataas) API to create the cards:
You can check out the source code for the project in this GitHub repo.
In traditional Redwood development, Cells had a QUERY function that contained the GraphQL query to fetch data from the database. To create a Server Cell, you export a data()
function instead. The data returned from this function will be passed to the success component.
Here is my Server Cell to get random cat images to use in the cards:
import Board from 'src/components/Board' import { cats } from 'src/services/cats' // The only difference between traditional and RSC RedwoodJS development // Replaces the QUERY function that contained the GraphQL query export const data = async () => { return { cats: await cats() } } export const Loading = () => <div>Loading...</div> export const Empty = () => <div>Empty</div> export const Failure = ({ error }) => <div>Error: {error.message}</div> export const Success = ({ cats }) => { return <Board cats={cats} /> }
To tell you the truth, there is not really much to look at here. In Redwood, a Cell manages data fetching and simplifies how you integrate data into the UI. Each state is handled by a different component.
Here is the cats
service that calls the Cataas API:
const swap = (array, i, j) => { const temp = array[i] array[i] = array[j] array[j] = temp } const shuffle = (array) => { const length = array.length for (let i = length; i > 0; i--) { const randomIndex = Math.floor(Math.random() * i) const currIndex = i - 1 swap(array, currIndex, randomIndex) } return array } export const cats = async () => { const res = await fetch('https://cataas.com/api/cats?type=square&limit=12') const data = await res.json() const doubled = data.flatMap((i) => [i, i]) const shuffled = shuffle(doubled) return shuffled }
All this does is call the API, double the results to have two of each cat, shuffle the array, and return the results to the cell. In traditional RedwoodJS with GraphQL, you put your services in the api
project folder. You don’t even have that with RSC. Instead, it’s in the web/src/services/cats.ts
folder.
So, we’ve seen two minor differences so far in structuring your projects, but ultimately, it’s pretty easy to get started. Now, if you are new to React Server Components, like I am, repeat to yourself while you are writing interactive code:
Working with Server Components requires thinking about your project differently. If you’re used to writing frontend React, it may take a while to get it right. I refactored my <Board />
component two times before I got it right. Fortunately, there are many ways to do the same thing.
I knew I needed state to keep track of found cards, clicked cards, and tries. I kept this in mind, but wrote the game logic in the board component while it still was a Server Component just to work it out. I got this error:
React.useState is not a function or its return value is not iterable
But I expected something, so I refactored.
I made the <Board />
component a Client Component by adding use client
to the top, but left the onClick
prop on the <Card />
component. After this change, there were no errors, but also no click events registering. So I refactored again.
Here is the final refactor:
'use client' import { useEffect } from 'react' import Card from 'src/components/Card' const Board = ({ cats }) => { const [chosenCards, setChosenCards] = React.useState([]) const [foundCards, setFoundCards] = React.useState([]) const [tries, setTries] = React.useState(0) const timeout = React.useRef(null) // Check card choices useEffect(() => { if (chosenCards.length === 2) { setTimeout(checkCards, 1000) } }, [chosenCards]) // Notify user of win useEffect(() => { if (foundCards.length === cats.length / 2) { alert(`You won in ${tries} tries`) } }, [foundCards, cats, tries]) const checkCards = () => { const [first, second] = chosenCards if (cats[first]._id === cats[second]._id) { setFoundCards((prev) => [...prev, cats[first]._id]) setChosenCards([]) return } timeout.current = setTimeout(() => { setChosenCards([]) }, 1000) } const handleCardClick = (index) => { if (foundCards.includes(cats[index]._id)) return if (chosenCards.length === 1 && chosenCards[0] !== index) { setChosenCards((prev) => [...prev, index]) setTries((tries) => tries + 1) } else { clearTimeout(timeout.current) setChosenCards([index]) } } const isFlipped = (index) => { return chosenCards.includes(index) } const isFound = (_id) => { return Boolean(foundCards.includes(_id)) } return ( <div style={{ width: '100vw', height: '100vh', display: 'flex', flexWrap: 'wrap', gap: '20px', margin: '20px', }} > {cats.map((cat, id) => ( // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions <div key={cat._id} onClick={() => handleCardClick(id)}> <Card image={ isFlipped(id) || isFound(cat._id) ? `https://cataas.com/cat?_id=${cat._id}` : 'https://d33wubrfki0l68.cloudfront.net/72b0d56596a981835c18946d6c4f8a968b08e694/82254/images/logo.svg' } /> </div> ))} </div> ) } export default Board
In the version that finally worked correctly, I wrapped the <Card />
in another <div />
that would handle the click events, and it only has an image
prop now:
const Card = ({ image }) => { return ( <div style={{ width: '240px', height: '240px', borderRadius: '10px', boxShadow: '0 0 10px rgba(0, 0, 0, 0.1)', display: 'flex', justifyContent: 'center', alignItems: 'center', }} > <img src={image} alt="cat" style={{ objectFit: 'cover', width: '200px', height: '200px' }} /> </div> ) } export default Card
Using RSCs makes you think more about how you structure your components. You need to decide which components will essentially be static and which components the user will interact with.
But you can nest Server Components inside of Client Components. In this project, the only Client Component is <Board />
because it uses state and its children (<Card />
components) are Server Components.
Check out the code for this project in this repo.
If you are used to developing with RedwoodJS, there are some workarounds you’ll need to use with RSCs. Some or all of these may not be needed once Bighorn becomes official.
For now, build and serve every time when you make changes. So:
Ctrl c
to stop the server.yarn rw build && yarn rw serve
again to see your changes.#
when replacing the current urlPreviously in RedwoodJS, you could modify the current URL without reloading the page by using { replace: true }
with navigate()
. This would change the address in history without server interaction.
But in the new canary version and Cambium RSC, this triggers a server call, redrawing the full page. This could cause transitions to trigger and page flicker with every change.
To avoid this, use a hash #
instead of a question mark ?
:
// classic RedwoodJS http://localhost:8910/products/shoes?color=red&size=10 // RedwoodJS with RSC http://localhost:8910/products/shoes#color=red&size=10
entries.ts
fileCurrently, there will be a /web/src/entries.ts
created in your project:
import { defineEntries } from '@redwoodjs/vite/entries' export default defineEntries( // getEntry async (id) => { switch (id) { case 'AboutPage': return import('./pages/AboutPage/AboutPage') case 'HomePage': return import('./pages/HomePage/HomePage') case 'ServerEntry': return import('./entry.server') default: return null } } )
Any route you define in the router (web/src/Routes.tsx
) will need its own entry there:
import { Router, Route, Set } from '@redwoodjs/router' import NavigationLayout from 'src/layouts/NavigationLayout' import NotFoundPage from 'src/pages/NotFoundPage' const Routes = () => { return ( <Router> <Set wrap={NavigationLayout}> <Route path="/" page={HomePage} name="home" /> <Route path="/about" page={AboutPage} name="about" /> </Set> <Route notfound page={NotFoundPage} /> </Router> ) } export default Routes
In the future, /web/src/entries.ts
will be gone and the framework will handle this part for you.
<Metadata>
to client components onlyRedwood’s <Metadata>
component relies on React Context, which uses state — and state is restricted to client-side components. So when you use this component to add meta tags in the head section of your pages, make sure you use it inside a component marked with use client
.
Even though Redwood renders components and services on the server, you won’t be able to use variables like __filename
and __dirname
. Redwood still uses the CJS format for modules, but is being upgraded to ESM.
For now, use path.resolve()
to construct the full fill path relative to its execution location, not its original source.
Redwood Bighorn’s canary provides a glimpse of its data fetching capabilities, but is currently “read-only”. Don’t worry if data modification (mutations) aren’t available yet — Server Actions, which unlock this functionality, are on the way.
RedwoodJS offers Server Cells for building dynamic UI elements. But nesting them directly in Layouts isn’t fully supported yet, so you may run into bugginess if you try. The Redwood team is working on a solution for this.
In Redwood, you can mark components for client-side rendering with use client
, but you can also nest server components within client components…if you do it right. If you do it wrong, the server components will be rendered in the browser as well, which overrides the whole reason for using RSC.
To do it the right way, isolate the client-side logic — like theme management — in a separate client component. This component can then act as a parent to your server components, allowing them to be imported as children
and rendered on the server as expected.
RedwoodJS isn’t the only framework that uses RSCs. It’s not even the first. Here are some other frameworks that support React Server Components:
In summary, here’s how RedwoodJS compares to Next.js, Waku, and Gatsby:
RedwoodJS | Next.js | Waku | Gatsby | |
---|---|---|---|---|
Project recommendations | Any size of application from solo projects to enterprise. | Small and medium-sized | Small and medium-sized. Not for enterprise. | Ideal for static websites or project prioritizing SEO and performance. |
Ready for production? | Traditional development is but RSCs aren’t just yet | Yes | Not yet | Yes |
Community and ecosystem | Growing community with active development. | Largest community in the list with many resources. | Relatively new framework with a small but growing community. | Large community and extensive ecosystem focused on static web development. |
The RedwoodJS team has some work to do before RSCs are ready for productions apps, but they are moving fast. They still need to:
But once that’s done, RedwoodJS could be a game-changer for building React applications that are not only quick at loading, but quick and easy to develop.
There’s no doubt that frontends are getting more complex. As you add new JavaScript libraries and other dependencies to your app, you’ll need more visibility to ensure your users don’t run into unknown issues.
LogRocket is a frontend application monitoring solution that lets you replay JavaScript errors as if they happened in your own browser so you can react to bugs more effectively.
LogRocket works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app’s performance, reporting metrics like client CPU load, client memory usage, and more.
Build confidently — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowuseState
useState
can effectively replace ref
in many scenarios and prevent Nuxt hydration mismatches that can lead to unexpected behavior and errors.
Explore the evolution of list components in React Native, from `ScrollView`, `FlatList`, `SectionList`, to the recent `FlashList`.
Explore the benefits of building your own AI agent from scratch using Langbase, BaseUI, and Open AI, in a demo Next.js project.
Demand for faster UI development is skyrocketing. Explore how to use Shadcn and Framer AI to quickly create UI components.