According to its documentation,
“Redwood is a full-stack web development framework designed to help you grow from side project to startup.”
Redwood features an end-to-end development workflow that weaves together the best part of React for developing pages and components, GraphQL as the query language for data, Prisma as ORM for accessing data, TypeScript for better programming, Jest for testing, and Storybook to aid with component development. It provides a full-stack development process in JAMStack.
Redwood is obsessed with developer experience and eliminating as much boilerplate as possible where existing libraries elegantly solve these problems, and where we don’t write our solution. The result has a Javascript development experience one can fall in love with!
Features
Let’s look at Redwood’s main basic features that make it look attractive as a full-fledged framework.
Project Structure: Two folders are generated as API and Web when creating a Redwood project. API folder contains the GraphQL back end of your project, which runs on any server. The Web folder contains React front-end code; Redwood uses React for the front end, and several features are built around Routing, Components, Forms, and Cells, including unique formatting structures.
Distinctive Command-Line: RedwoodJS CLI allows you to generate any file you need in the directory you specify. It includes two entry points,
rw
andrwt
, which stand for developing an application and contributing to the frameworks, respectively. Also, you can use the CLI for migration and scaffolding purposes.Routing: Redwood Router is a customized version of React Router. It allows developers to track Routes with their corresponding pages by listing all the Routes in a single Routing file. The Named Routes feature allows you to add a reference name to each Route as you prefer. There are three main prop types in React Router name (used to call the route component from the link component), path (used to match the URL path to the route), and page (a prop that indicates the relevant component that needs to be rendered when the path is matched).
1// web/src/Routes.js2import { Router, Route } from ‘@redwoodjs/router’3const Routes = () => {4 return (5 <Router>6 <Route path=”/” page={HomePage} name=”home” />7 <Route path=”/users/{type}” page={UsersPage} name=”users” /> </Router>8 )9}10// web/src/components/Admin/Admin.js11import { Link, routes } from ‘@redwoodjs/router’12const Admin = () => {13 return (14 <h1><Link to={routes.home()}>My CRM</Link></h1>15 <Link to={routes.users({type: “admin”})}>View Admins</Link>16 )17}
- Data Fetch with Cells: RedwoodJS cells are considered higher-level components that can handle data for you. If you are using RedwoodJS cells, you probably need to write the query then Redwood will do the rest for you. The data fetching mechanism is quite different from the typical traditional system. Redwood provides an asynchronous tool to fetch data where the front end and back end process their task separately, which makes the front end load without waiting for the data to be fetched. Let’s look at Redwood cells; they contain four states named
Loading
,Empty
,Error
, andSuccess
, which are automatically rendered depending on the state of your cell.
1// web/src/components/UsersCell/UsersCell.js2export const QUERY = gql`3 query USERS {4 users {5 id6 name7 }8 }910export const Loading = () => <div>Loading users...</div>11export const Empty = () => <div>No users yet!</div>12export const Failure = ({ message }) => <div>Error: {message}</div>13export const Success = ({ users }) => {14 return (15 <ul>16 { users.map(user => (17 <li>{user.id} | {user.name}</li>18 ))}19 </ul>20 )21}
- Integration with regular HTML: Redwood has identified the issue of React Forms and its complexities compared to a typical HTML Form, and has provided a solution which is a helper method as a wrapper to React Forms, around
react-hook-form
. With this method, it’s easier to validate client & server-side where you need to put the required styles to the imported input fields as follows:
1import { Form, Label, TextAreaField, FieldError, Submit } from “@redwoodjs/web”2export const Comment = () => {3 const onSubmit = (data) => {4 console.info(`Submitted: ${data}`)5 }6 return (7 <Form onSubmit={onSubmit}> <Label name=”comment” errorStyle={{ color: “red” }} />8 <TextAreaField9 name=”comment”10 errorStyle={{ borderColor: “red” }}11 validation={{ required: true }}12 />13 <FieldError name=”comment” errorStyle={{ color: “red” }} />14 <Submit>Send</Submit>15 </Form>16 )17}
Open Source Session Replay
OpenReplay is an open-source, session replay suite that lets you see what users do on your web app, helping you troubleshoot issues faster. OpenReplay is self-hosted for full control over your data.
Start enjoying your debugging experience - start using OpenReplay for free.
How Redwood works
In a typical setting, Redwood applications are split into two parts: a front end and a back end. This is represented as two JS/TS projects within a single monorepo. The front-end project is called the web side, and the back-end project is called the API side. The web side will end up running in the user’s browser, while code on the API side will run on the server.
Note: Redwood refers to ‘front end’ as ‘web’. This is because Redwood conceives of a world where you may have other sides like ‘mobiles’, ‘desktop’, ‘cli’, etc. But for the sake of this article, we’ll be referring to it as the front end.
The front end is built with React. Redwood’s router makes it simple to map URL paths to React ‘Page’ components. Pages may contain a ‘Layout’ component to wrap content and ‘Cells’ and regular React components. The Cells allow you to declaratively manage the lifecycle of a component that fetches and displays data. Other Redwood utility components make it insignificant to integrate forms and various basic needs.
The back end is an implementation of GraphQL API. Your business logic is organized into ‘services’ that represent their internal API and can be called both from external GraphQL requests and other internal services. Redwood can automatically connect your internal services with Apollo, reducing the amount of boilerplate you have to write.
Before we move on to building our Redwood project, let’s take a look at some of the basic commands of RedwoodJS CLI:
yarn rw g page
: generates a page componentyarn rw prisma migrate dev
: Runs migrations on our applicationyarn rw g sdl
: generates a GraphQL schema definition fileyarn rw g service
: generates the service filesyarn rw g cell
: generates cell componentyarn rw dev
: runs Redwood dev server on http://localhost:8910/.
Building an app
We need to illustrate how to use RedwoodJS to build an app. The Todo app will be able to take notes and save those notes on the JSON server. Note: Credit to RedwoodJS for the example.
First steps
To create the project, all we have to do is run the following commands:
1yarn create redwood-app todo-app2cd todo-app3yarn rw dev
Let’s take a glance at our folder structure:
1todo-app2 │3 ├── api4 │ ├── db5 │ │ ├── schema.prisma6 │ │ └── seed.js7 │ └── src8 │ ├── functions9 │ │ └── graphql.js10 │ ├── graphql11 │ ├── lib12 │ │ └── db.js13 │ └── services14 └── web15 ├── public16 │ ├── README.md17 │ ├── favicon.png18 │ └── robots.txt19 └── src20 ├── Routes.js21 ├── components22 ├── index.css23 ├── index.html24 ├── App.js25 ├── layouts26 └── pages27 ├── FatalErrorPage28 │ └── FatalErrorPage.js29 └── NotFoundPage30 └── NotFoundPage.js
Defining data models
Now we have to add our Todo model to the schema.prisma file.
1model Todo {2 id Int @id @default(autoincrement())3 body String4 status String @default("off")5}
Note: Run yarn rw prisma migrate
, which will prompt for the name of the migrations; just type added_todo and hit the enter button. Redwood will create and run the migrations.
We’ll create services that will be used to carry out CRUD operations on our todo table in the db:
1yarn rw g service todo2yarn rw g sdl
This command will create the files:
- api/src/graphql/todos.sdl.js: contains the GraphQL schema definitions of our GraphQL endpoints.
- api/src/services/todos/todos.js: our service file contains all query and mutation resolver functions.
Creating the page and components
Our app will have just a page, i.e., the HomePage. The homepage will show the to-dos in the database and the input field.
1yarn rw g page home/ todos
This command creates the to-do page. The main file is located at web/src/pages/HomePage/HomePage.js. The page will map to the URL path ”/” home. We’ll also create a TodoListCell: yarn rw g cell list todo
. It will generate files in the components folder. The main file is located at web/src/components/TodoListCell/TodoListcell.js.
We modify the code to this:
1import styled from 'styled-components'2import TodoItem from 'src/components/TodoItem'3import { useMutation } from '@redwoodjs/web'4export const QUERY = gql`5 query TODOS {6 todos {7 id8 body9 status10 }11 }12`13const UPDATE_TODO_STATUS = gql`14 mutation TodoListCell_CheckTodo($id: Int!, $status: String!) {15 updateTodoStatus(id: $id, status: $status) {16 id17 __typename18 status19 }20 }21`22export const Loading = () => <div>Loading...</div>23export const Empty = () => <div></div>24export const Failure = () => <div>Oh no</div>25export const Success = ({ todos }) => {26 const [updateTodoStatus] = useMutation(UPDATE_TODO_STATUS)27 const handleCheckClick = (id, status) => {28 updateTodoStatus({29 variables: { id, status },30 optimisticResponse: {31 __typename: 'Mutation',32 updateTodoStatus: { __typename: 'Todo', id, status: 'loading' },33 },34 })35 }36 const list = todos.map((todo) => (37 <TodoItem key={todo.id} {...todo} onClickCheck={handleCheckClick} />38 ))39 return <SC.List>{list}</SC.List>40}41export const beforeQuery = (props) => ({42 variables: props,43})44const SC = {}45SC.List = styled.ul`46 padding: 0;47 `
We’ll generate AddTodoControl
component, which will have an input form with a placeholder and a submit button to submit our todos. Now, let’s scaffold the AddTodoControl component:
1yarn rw g component AddTodoControl
It creates a AddTodocontrol
component located at web/src/components/AddTodoControl/AddTodoControl.js and our code should look like this:
1import styled from 'styled-components'2import { useState } from 'react'3import Check from 'src/components/Check'4const AddTodoControl = ({ submitTodo }) => {5 const [todoText, setTodoText] = useState('')6 const handleSubmit = (event) => {7 submitTodo(todoText)8 setTodoText('')9 event.preventDefault()10 }11 const handleChange = (event) => {12 setTodoText(event.target.value)13 }14 return (15 <SC.Form onSubmit={handleSubmit}>16 <Check type="plus" />17 <SC.Body>18 <SC.Input19 type="text"20 value={todoText}21 placeholder="Memorize the dictionary"22 onChange={handleChange}23 />24 <SC.Button type="submit" value="Add Item" />25 </SC.Body>26 </SC.Form>27 )28}29const SC = {}30SC.Form = styled.form`31 display: flex;32 align-items: center;33`34SC.Body = styled.div`35 border-top: 1px solid #efefef;36 border-bottom: 1px solid #efefef;37 width: 100%;38`39SC.Input = styled.input`40 border: none;41 font-size: 18px;42 font-family: 'Inconsolata', monospace;43 padding: 10px 0;44 width: 75%;4546 ::placeholder {47 color: #9e9595;48 }49`50SC.Button = styled.input`51 float: right;52 margin-top: 5px;53 border-radius: 6px;54 background-color: #f75d52;55 padding: 5px 15px;56 color: white;57 border: 0;58 font-size: 18px;59 font-family: 'Inconsolata', monospace;60 :hover {61 background-color: black;62 cursor: pointer;63 }64`65export default AddTodoControl
We’ll create our TodoItem with the command yarn rw g component TodoItem
and this component consists of our style attributes for each todo body, so we’re going to implement things like padding, borders, and lines with the code below:
1import styled from 'styled-components'2import Check from 'src/components/Check'3const TodoItem = ({ id, body, status, onClickCheck }) => {4 const handleCheck = () => {5 const newStatus = status === 'off' ? 'on' : 'off'6 onClickCheck(id, newStatus)7 }8 return (9 <SC.Item>10 <SC.Target onClick={handleCheck}>11 <Check type={status} />12 </SC.Target>13 <SC.Body>{status === 'on' ? <s>{body}</s> : body}</SC.Body>14 </SC.Item>15 )16}17const SC = {}18SC.Item = styled.li`19 display: flex;20 align-items: center;21 list-style: none;22`23SC.Target = styled.div`24 cursor: pointer;25`26SC.Body = styled.div`27 list-style: none;28 font-size: 18px;29 border-top: 1px solid #efefef;30 padding: 10px 0;31 width: 100%;32`33export default TodoItem
The Check component consists of our icons like on, off, plus icons. To create the check component, run yarn rw g component
. Input the following code in our Check component:
1import styled from 'styled-components'2import IconOn from './on.svg'3import IconOff from './off.svg'4import IconPlus from './plus.svg'5import IconLoading from './loading.svg'6const map = {7 on: <IconOn />,8 off: <IconOff />,9 plus: <IconPlus />,10 loading: <IconLoading />,11}12const Check = ({ type }) => {13 return <SC.Icon>{map[type]}</SC.Icon>14}15const SC = {}16SC.Icon = styled.div`17 margin-right: 15px;18`19export default Check
We need an AddTodo component to add new todos.
1import { useMutation } from '@redwoodjs/web'2import AddTodoControl from 'src/components/AddTodoControl'3import { QUERY as TODOS } from 'src/components/TodoListCell'45const CREATE_TODO = gql`6 mutation AddTodo_CreateTodo($body: String!) {7 createTodo(body: $body) {8 id9 __typename10 body11 status12 }13 }14`15const AddTodo = () => {16 const [createTodo] = useMutation(CREATE_TODO, {1718 update: (cache, { data: { createTodo } }) => {19 const { todos } = cache.readQuery({ query: TODOS })20 cache.writeQuery({21 query: TODOS,22 data: { todos: todos.concat([createTodo]) },23 })24 },25 })26 const submitTodo = (body) => {27 createTodo({28 variables: { body },29 optimisticResponse: {30 __typename: 'Mutation',31 createTodo: { __typename: 'Todo', id: 0, body, status: 'loading' },32 },33 })34 }35 return <AddTodoControl submitTodo={submitTodo} />36}37export default AddTodo
This is our final project.
Conclusion
There are many other reasons to get attracted to RedwoodJS.
- Opinionated defaults for formatting, file organization, Webpack, Babel, and more
- Generators for pages, layouts, cells, SDL, services, etc.
- Forms with easy client-and/or server-side validation and error handling
- Database and Data migrations
All these and many more make RedwoodJS unique to the core.