Leveraging React Server Components in RedwoodJS
Tags: react, webdev
Published: 2024-05-16 14:03:59

**Written by [Stephan Miller](https://blog.logrocket.com/author/stephan-miller/)✏️**

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](https://redwoodjs.com/blog/rsc-now-in-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](https://github.com/eristoddle/redwoodjs-rsc-cat-memory-app), and discuss some of the workarounds you'll have to use while Bighorn is still in active development.

## RedwoodJS Bighorn and React Server Components

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.

## Getting started with RSCs in RedwoodJS

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.

### Setting up your development environment

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:

```bash
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:

```bash
yarn install
```

If you get an error when running the last command, run this:

```shell
corepack enable
```

To use React Server Components with RedwoodJS, you must install these experimental features:

```bash
yarn rw experimental setup-streaming-ssr -f
yarn rw experimental setup-rsc
```

Then run the following commands to build and start the app:

```bash
yarn rw build
yarn rw serve
```

This is the step where I ran into this error:

```plaintext
".../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:

```javascript
import data from './data.json' assert { type: 'json
```

Changed to this:

```javascript
import data from './data.json' with { type: 'json
```

[Here are more details](https://github.com/redwoodjs/redwood/issues/10492) 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/](http://localhost:8910/): ![Redwoodjs App Run Locally After Dev Environment Setup With Experimental Features Including React Server Components](https://blog.logrocket.com/wp-content/uploads/2024/05/img1-RedwoodJS-app-run-locally-dev-environment-setup-experimental-features.png)

### Building a cat memory game with RedwoodJS and RSCs

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](https://blog.logrocket.com/how-to-build-full-stack-app-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](https://cataas.com/) (Cataas) API to create the cards: ![Memory Card Game Built With The Cataas Api, Redwoodjs, And React Server Components](https://blog.logrocket.com/wp-content/uploads/2024/05/img2-Memory-card-game-Cataas-API-RedwoodJS-RSCs.jpeg) You can check out the source code for the project in [this GitHub repo](https://github.com/eristoddle/redwoodjs-rsc-cat-memory-app).

### Server Cells and Services

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:

```javascript
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:

```javascript
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:

*   Server Components never re-render
*   Don't use state in Server Components

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:

```plaintext
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:

```javascript
'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:

```javascript
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](https://github.com/eristoddle/redwoodjs-rsc-cat-memory-app).

## Current caveats to using RSCs in RedwoodJS

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.

### You can't use the dev server yet

For now, build and serve every time when you make changes. So:

*   Make some changes.
*   Hit `Ctrl c` to stop the server.
*   Run `yarn rw build && yarn rw serve` again to see your changes.

### Use a hash `#` when replacing the current url

Previously 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 `?`:

```plaintext
// classic RedwoodJS
http://localhost:8910/products/shoes?color=red&size=10

// RedwoodJS with RSC
http://localhost:8910/products/shoes#color=red&size=10
```

### Manually edit the routing `entries.ts` file

Currently, there will be a `/web/src/entries.ts` created in your project:

```typescript
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:

```typescript
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.

### Add `<Metadata>` to client components only

Redwood'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`.

### No access to core Node vars

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.

### No Server Actions yet

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.

### No Server Cells in Layouts yet

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.

### Rendering server components in client components

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 vs. other RSC-compatible frameworks

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:

*   **Next.js:** This is probably the most well-known framework [using RSCs](https://blog.logrocket.com/react-server-components-next-js-13/) because it's selling point is its server-side rendering capabilities. It is also the only one mentioned in [React documentation](https://react.dev/learn/start-a-new-react-project#which-features-make-up-the-react-teams-full-stack-architecture-vision). [Next.js](https://blog.logrocket.com/diving-into-server-actions-next-js-14/) also provides a variety of server rendering strategies, including static (SSG), incremental (ISR), and dynamic ([SSR](https://blog.logrocket.com/implementing-ssr-next-js-dynamic-routing-prefetching/)) rendering
*   **Waku:** [Waku is a minimal React framework](https://blog.logrocket.com/waku-vs-next-js/) designed for small- to medium-sized projects. It was built from the ground up to use RSCs. However, Waku is still in active development, so features may be missing. It’s not recommended for production applications yet, but they are working toward a stable release
*   **Gatsby:** [Gatsby](https://blog.logrocket.com/gatsby-ultimate-guide-examples/) is traditionally known for its static site generation capabilities and React integration, but you can also use SSR. When you do, Gatsby uses React Server Components when you need to generate HTML dynamically

In summary, here’s how RedwoodJS compares to Next.js, Waku, and Gatsby:

<table>

<thead>

<tr>

<th></th>

<th>RedwoodJS</th>

<th>Next.js</th>

<th>Waku</th>

<th>Gatsby</th>

</tr>

</thead>

<tbody>

<tr>

<td>Project recommendations</td>

<td>Any size of application from solo projects to enterprise.</td>

<td>Small and medium-sized</td>

<td>Small and medium-sized. Not for enterprise.</td>

<td>Ideal for static websites or project prioritizing SEO and performance.</td>

</tr>

<tr>

<td>Ready for production?</td>

<td>Traditional development is but RSCs aren't just yet</td>

<td>Yes</td>

<td>Not yet</td>

<td>Yes</td>

</tr>

<tr>

<td>Community and ecosystem</td>

<td>Growing community with active development.</td>

<td>Largest community in the list with many resources.</td>

<td>Relatively new framework with a small but growing community.</td>

<td>Large community and extensive ecosystem focused on static web development.</td>

</tr>

</tbody>

</table>

## Conclusion

The RedwoodJS team has some work to do before RSCs are ready for productions apps, but they are moving fast. They still need to:

*   Get SSR working with RSC so the initial page load contains pre-rendered HTML
*   Complete Server Actions so you can save data to your database
*   Modify auth to work with RSCs
*   Update generators to create RSC or GraphQL templates dynamically

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.

---

##Are you adding new JS libraries to build new features or improve performance? What if they’re doing the opposite?

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](https://lp.logrocket.com/blg/javascript-signup) 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 Signup](https://blog.logrocket.com/wp-content/uploads/2019/10/errors-screenshot.png)](https://lp.logrocket.com/blg/javascript-signup)

[LogRocket](https://lp.logrocket.com/blg/javascript-signup) 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](https://lp.logrocket.com/blg/javascript-signup).