What is Redwood?
What follows is a high-level description of Redwood and how it works. If you want to get right to the meat and potatoes of building something, skip ahead to Chapter 1.
Redwood is a React framework with lots of pre-installed packages and configuration that makes it easy to build full-stack web applications.
Now that the elevator pitch is out of the way, what does that actually mean? At its core, Redwood is React plus a bunch of stuff that makes your life as a developer easier. Some of that stuff includes:
- GraphQL
- Prisma
- Jest
- Storybook
- vite
- Babel
- Typescript
What do we mean when we say a "full-stack web application?" We're talking about your classic web app: a UI that's visible in the browser (the frontend), backed by a server and database (the backend). Until React Server Components came along (more on those later) React had no idea a server and/or database existed: it was up to you to somehow get data into your app. Maybe this was done with a fetch()
or in a build step which would pre-bake some of the data needed right into your components. However the data got there, it wasn't an ideal solution.
One of the core principals behind Redwood was that getting data from the backend should be as simple as possible, going so far as to create conventions around it so that retrieving data for display in a component was as easy as adding a couple of lines of code directly into the component itself. Oh and while we're at it, Redwood will automatically show a loading message while waiting for the data, a different state if there's an error, and even a separate message if the data returned from the server is empty (the classic "blank slate").
How a Redwood App Works
A Redwood app is actually two apps: a frontend (that's the React part) and a backend, which is your server and talks to a database and other third party systems. Your app is technically a monorepo with two top-level directories: web
containing the frontend code and api
containing the backend.
You can start them both with a single command: yarn redwood dev
The Frontend
The Router
When you open your web app in a browser, React does its thing initializing your app and monitoring the history for changes so that new content can be shown. Redwood features a custom, declarative Router that lets you specify URLs and the requisite pages (just a React component) will be shown. A simple routes file may look something like:
import { Route, Router, Set, PrivateSet } from '@redwoodjs/router'
import ApplicationLayout from 'src/layouts/ApplicationLayout'
import { useAuth } from './auth'
const Routes = () => {
return (
<Router useAuth={useAuth}>
<Set wrap={ApplicationLayout}>
<Route path="/login" page={LoginPage} name="login" />
<Route path="/signup" page={SignupPage} name="signup" />
<PrivateSet unauthenticated="login">
<Route path="/dashboard" page={DashboardPage} name="dashboard" />
<Route path="/products/{sku}" page={ProductsPage} name="products" />
</PrivateSet>
</Set>
<Route path="/" page={HomePage} name="home" />
<Route notfound page={NotFoundPage} />
</Router>
)
}
You can probably get a sense of how all of this works without ever having seen a Redwood route before! Some routes can be marked as <PrivateSet>
and will not be accessible without being logged in. Others can be wrapped in a "layout" (again, just a React component) to provide common styling shared between pages in your app.
Prerender
If you have content on your page that can be purely static (like public facing marketing-focused pages) you can simply add the prerender
attribute to your route and that page will be completely rendered (no matter how deeply nested the internal components go) into an HTML page. This page loads instantly, but still contains the JS needed to include React. Once React loads, the page is rehydrated and becomes interactive.
You can also prerender pages that contain variables pulled from the URL, like the /products/{sku}
route above. Redwood will iterate through all available skus and generate a page for each.
This is Redwood's version of static site generation, aka SSG.
Authentication
The <PrivateSet>
route limits access to users that are authenticated, but how do they authenticate? Redwood includes integrations to many popular third party authentication hosts (including Auth0, Supabase and Clerk). You can also host your own auth, or write your own custom authentication option. If going self-hosted, we include login, signup, and reset password pages, as well as the option to include TouchID/FaceID and third party biometric readers!
Once authenticated, how do you know what a user is allowed to do or not do? Redwood includes helpers for role-based access control that integrates on both the front- and backend.
The homepage is accessible without being logged in, browsing to /
will load the HomePage
page (component) which itself is just composed of more React components, nothing special there. But, what if the homepage, say, displayed some testimonials from the database? Ahh, now things are getting interesting. Here's where Redwood's handpicked selection of technologies start to take the spotlight.
GraphQL
Redwood uses GraphQL as the glue between the front- and backends: whenever you want data from the server/database, you're going to retrieve it via GraphQL. Now, we could have just given you raw access to some GraphQL library and let you make those calls yourself. We use Apollo Client on the frontend and Apollo provides hooks like useQuery() and useMutation() to retrieve and set data, respectively. But Redwood has a much deeper integration.
What if you could have a component that was not only responsible for its own display but even its own data retrieval? Meaning everything that component needed in order to display itself could all be self-contained. That includes the code to display while the data is loading, or if something goes wrong. These kinds of uber-components are real, and Redwood calls "cells."
Cells
A cell is still just a React component (also called a single file component), it just happens to follow a couple of conventions that make it work as described above:
- The name of the file ends in `Cell"
- The file exports several named components, at the very least one named
QUERY
and another namedSuccess
- The file can optionally export several other components, like
Loading
,Failure
andEmpty
. You can probably guess what those are for!
So, any time React is about to render a cell, the following lifecycle occurs:
- The
Loading
component is displayed - A
useQuery()
hook is fired, using the exportedQUERY
- Assuming the data returns successfully, the
Success
component is rendered with one of the props being the data returned fromuseQuery()
As an alternative to step 3, if something went wrong then Failure
is rendered. If the query returned null
or an empty array, the Empty
component is rendered. If you don't export either of those then Success
will be rendered and it would be up to you to show the error or empty state through conditional code.
Going back to our testimonals hypothetical, a cell to fetch and display them may look something like:
export const QUERY = gql`
query GetTestimonials {
testimonials {
id
author
quote
}
}
`
export const Loading = () => <div>Loading...</div>
export const Failure = ({ error }) => <div>An error occured! {error.message}</div>
export const Success = ({ testimonials }) => {
return (
<ul>
{testimonials.map((test) => {
<li key={test.id}>{test.quote} — {test.author}</li>
})}
</ul>
)
}
(In this case we don't export Empty
so that if there aren't any testimonials, that section of the final page won't render anything, not even indicating to the user that something is missing.)
If you ever create additional clients for your server (a mobile app, perhaps) you'll be giving yourself a huge advantage by using GraphQL from the start.
Oh, and prerendering also works with cells! At build time, Redwood will start up the GraphQL server and make requests, just as if a user was accessing the pages, rendering the result to plain HTML, ready to be loaded instantly by the browser.
Apollo Cache
The Apollo Client library also intelligently caches the results of that QUERY
above, and so if the user browses away and returns to the homepage, the Success
component is now rendered immediately from the cache! Simultaneously, the query is made to the server again to see if any data has changed since the cache was populated. If so, the new data is merged into the cache and the component will re-render to show any new testimonials since the last time it was viewed.
So, you get performance benefits of an instant display of cached data, but with the guarantee that you won't only see stale data: it's constantly being kept in sync with the latest from the server.
You can also directly manipulate the cache to add or remove entries, or even use it for state management.
If you're familiar with GraphQL then you know that on the backend you define the structure of data that GraphQL queries will return with "resolvers." But GraphQL itself doesn't know anything about talking to databases. How does the raw data in the database make it into those resolvers? That's where our next package comes in.