Building a Todo App with React Server

Moritz Roessler
5 min readApr 14, 2023

--

Introduction

React Server is an innovative open-source project that bridges the gap between server and client, allowing you to write your server-side code as TSX components and consume them on the frontend using GraphQL. In this tutorial, we’ll walk through the process of setting up a React Server and creating a simple todo list component.

Visit the documentation and examples at https://state-less.cloud

Prerequisites

Before we begin, ensure that you have the following installed on your local machine:

  • Node.js (version 16 or higher)
  • npm (version 6 or higher)

Setting up a new project

Get a Server running

git clone https://github.com/state-less/clean-starter.git -b react-server my-todo-app
cd my-todo-app
git remote remove origin
yarn install
yarn start

Get a Client running

Create a new vite project and choose React as framework and TypeScript as variant.

yarn create vite todo-app-frontend

Now go to the newly created folder, install the dependencies and add @apollo/client and state-less/react-client to your project and start the server.

cd todo-app-frontend
yarn
yarn add @mui/material @mui/icons-material
yarn add @emotion/react @emotion/styled
yarn add @apollo/client state-less/react-client
yarn dev

Instantiate a GraphQl client

In order to connect to our backend, we need to create a GraphQl client. Create a new file under todo-app-frontend/src/lib/client.ts and paste the following content.

import { ApolloClient, InMemoryCache, split, HttpLink } from "@apollo/client";
import { WebSocketLink } from "@apollo/client/link/ws";
import { getMainDefinition } from "@apollo/client/utilities";// Create an HTTP link
const localHttp = new HttpLink({
uri: "http://localhost:4000/graphql",
});
// Create a WebSocket link
const localWs = new WebSocketLink({
uri: `ws://localhost:4000/graphql`,
options: {
reconnect: true,
},
});
// Use the split function to direct traffic between the two links
const local = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
localWs,
localHttp
);
// Create the Apollo Client instance
export const localClient = new ApolloClient({
link: local,
cache: new InMemoryCache(),
});

export default localClient;

This sets up a new GraphQl client with subscriptions which will be used by the React Server client. The subscriptions are needed in order to make your app reactive.

Note: For now you need to manually create this file, but it will later be created by an initializer or react-client will provide a way to bootstrap the graphql client by providing an url pointing to a react server. For now you need to manually create and provide a GraphQl client.

The Backend

Create a file under my-todo-app/src/components/Todos.tsx and paste the following content.

import { Scopes, useState } from '@state-less/react-server';
import { v4 } from 'uuid';
import { ServerSideProps } from './ServerSideProps';

type TodoObject = {
id: string | null;
title: string;
completed: boolean;
};

export const Todo = ({ id, completed, title }: TodoObject) => {
const [todo, setTodo] = useState<TodoObject>(
{
id,
completed,
title,
},
{
key: `page${id}`,
scope: Scopes.Client,
}
);

const toggle = () => {
setTodo({ ...todo, completed: !todo.completed });
};

return <ServerSideProps key={`${id}-todo`} {...todo} toggle={toggle} />;
};

export const Todos = () => {
const [todos, setTodos] = useState<TodoObject[]>([], {
key: 'todos',
scope: Scopes.Client,
});

const addTodo = (todo: TodoObject) => {
const id = v4();
const newTodo = { ...todo, id };

if (!isValidTodo(newTodo)) {
throw new Error('Invalid todo');
}
setTodos([...todos, newTodo]);
};

const removeTodo = (id: string) => {
setTodos(todos.filter((todo) => todo.id !== id));
};
return (
<ServerSideProps key="todos-props" add={addTodo} remove={removeTodo}>
{todos.map((todo) => (
<Todo {...todo} />
))}
</ServerSideProps>
);
};

const isValidTodo = (todo): todo is TodoObject => {
return todo.id && todo.title && 'completed' in todo;
};

Go to the my-todo-app/src/index.ts and add the Todo component to your Server entrypoint.

export const reactServer = (
<Server key="server">
<Todos key="todos" />
</Server>
);
import { ApolloServer } from 'apollo-server-express';
import { makeExecutableSchema } from 'graphql-tools';
import { execute, subscribe } from 'graphql';
import { createServer } from 'http';
import express from 'express';
import { SubscriptionServer } from 'subscriptions-transport-ws';
import {
Server,
render,
Dispatcher,
} from '@state-less/react-server';

import { pubsub, store } from './instances';

import { resolvers } from './resolvers';
import { typeDefs } from './schema';
import { Todos } from './components/Todos';

Dispatcher.getCurrent().setStore(store);
Dispatcher.getCurrent().setPubSub(pubsub);

const app = express();
const PORT = 4000;

const schema = makeExecutableSchema({
typeDefs,
resolvers,
});

const apolloServer = new ApolloServer({
schema,
context: ({ req }) => {
const { headers } = req;
return { headers };
},
});

// Create a HTTP server
const httpServer = createServer(app);

// Create a WebSocket server for subscriptions
SubscriptionServer.create(
{
schema,
execute,
subscribe,
onConnect: () => {
console.log('Client connected');
},
onDisconnect: () => {
console.log('Client disconnected');
},
},
{
server: httpServer,
path: apolloServer.graphqlPath,
}
);

export const reactServer = (
<Server key="server">
<Todos key="todos" />
</Server>
);

render(reactServer, null, null);

(async () => {
await apolloServer.start();
apolloServer.applyMiddleware({ app });
httpServer.listen(PORT, () => {
console.log(`Server listening on port ${PORT}.`);
});
})();

Start the backend server with yarn start

The Frontend

Create a file under todo-app-frontend\src\components\TodoList.tsx

import {  useState } from 'react';
import {
Alert,
Box,
Button,
Card,
CardHeader,
Checkbox,
IconButton,
List,
ListItem,
ListItemIcon,
ListItemSecondaryAction,
ListItemText,
TextField,
} from '@mui/material';
import EditIcon from '@mui/icons-material/Edit';
import RemoveCircleIcon from '@mui/icons-material/RemoveCircle';
import { useComponent } from '@state-less/react-client';

export const TodoApp = (props) => {
const [component, { loading, error }] = useComponent('todos', {});
const [title, setTitle] = useState('');
const [edit, setEdit] = useState(false);

if (loading) {
return null;
}

return (
<Card>
{error && <Alert severity="error">{error.message}</Alert>}
<CardHeader
title={
<Box sx={{ display: 'flex', alignItems: 'center' }}>
<TextField
value={title}
label="Title"
onChange={(e) => setTitle(e.target.value)}
/>
<Button
onClick={() => {
setTitle('');
component.props.add({ title, completed: false });
}}
>
Add
</Button>
</Box>
}
action={
<IconButton onClick={() => setEdit(!edit)}>
<EditIcon />
</IconButton>
}
></CardHeader>
<List>
{component?.children.map((todo, i) => (
<TodoItem
key={i}
todo={todo}
edit={edit}
remove={component?.props?.remove}
/>
))}
</List>
</Card>
);
};

const TodoItem = (props) => {
const { todo, edit, remove } = props;

// Hydarate the call with the data from the parent component
const [component, { loading }] = useComponent(todo.key, { data: todo });

return (
<ListItem>
{edit && (
<ListItemIcon>
<IconButton onClick={() => remove(component.props.id)}>
<RemoveCircleIcon />
</IconButton>
</ListItemIcon>
)}
<ListItemText
primary={component.props.title}
sx={{ textDecoration: component.props.completed ? 'line-through' : '' }}
/>
<ListItemSecondaryAction>
<Checkbox
checked={component?.props.completed}
onClick={() => {
console.log('TODO', todo);
component?.props.toggle();
}}
/>
</ListItemSecondaryAction>
</ListItem>
);
};

Replace the content of todo-app-frontend\src\App.tsx

import { ApolloProvider } from '@apollo/client'
import './App.css'
import { TodoApp } from './components/TodoList'
import localClient from './lib/client'

function App() {
return (
<div className="App">
<ApolloProvider client={localClient}>
<TodoApp />
</ApolloProvider>
</div>
)
}

export default App

Start the frontend with yarn dev and visit http://127.0.0.1:5173/ you should see a working todo list that persists after a page reload.

Conclusion

Congratulations! You’ve successfully set up a React Server and created a simple todo app. This tutorial only scratches the surface of what’s possible with React Server. We encourage you to explore the documentation and experiment with creating more complex components and applications.

We hope this tutorial has been helpful, and we look forward to seeing what you build with React Server!

--

--