At this point, we have all the backend (database table, permissions, and an API) needed to store and retrieve todos from any frontend app. It is now time to start building the actual frontend app!
We are going to use create-react-app because it is the easiest way to get started, and it offers a modern build setup with no configuration.
Create a new react app:
$ npx create-react-app nhost-todos
Once completed, change the directory to the new project and install these packages. The packages will be used to send GraphQL requests to the Nhost backend.
$ cd nhost-todos
$ npm install @nhost/react-apollo@v1.1.3 @apollo/client graphql react-router-dom
Create a jsconfig.json
file in the root of your project to make imports easier.
{
"compilerOptions": {
"baseUrl": "src"
},
"include": ["src"]
}
Start the app.
$ npm start
NhostApolloProvider
comes from @nhost/react-apollo and deals with all the boilerplate for having ApolloClient
configured to communicate securely with Nhost.
Import NhostApolloProvider
and wrap App
so that GraphQL is available everywhere in your app.
Replace
https://hasura-xxx.nhost.app/v1/graphql
with your project's GraphQL API endpoint. You can find the endpoint in your project's dashboard.
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { NhostApolloProvider } from "@nhost/react-apollo";
ReactDOM.render(
<React.StrictMode>
<NhostApolloProvider gqlEndpoint="https://hasura-xxx.nhost.app/v1/graphql">
<App />
</NhostApolloProvider>
</React.StrictMode>,
document.getElementById("root")
);
Now we can fetch data!
Replace all content of src/App.js
with the following code:
import React from "react";
import { gql, useQuery } from "@apollo/client";
const GET_TODOS = gql`
query {
todos {
id
created_at
name
is_completed
}
}
`;
function App() {
const { data, loading } = useQuery(GET_TODOS);
if (loading) {
return <div>Loading</div>;
}
return (
<div>
{!data ? (
"No todos"
) : (
<ul>
{data.todos.map((todo) => {
return <li key={todo.id}>{todo.name}</li>;
})}
</ul>
)}
</div>
);
}
export default App;
Your two previous todos now appear in your app.
Let's move on and add functionality to insert new todos.
We need to create a GraphQL mutation and add a simple form for inserting todos.
import React, { useState } from "react";
import { gql, useQuery, useMutation } from "@apollo/client";
const GET_TODOS = gql`
query {
todos {
id
created_at
name
is_completed
}
}
`;
const INSERT_TODO = gql`
mutation($todo: todos_insert_input!) {
insert_todos(objects: [$todo]) {
affected_rows
}
}
`;
function App() {
const { data, loading } = useQuery(GET_TODOS);
const [insertTodo] = useMutation(INSERT_TODO);
const [todoName, setTodoName] = useState("");
if (loading) {
return <div>Loading</div>;
}
return (
<div>
<div>
<form
onSubmit={async (e) => {
e.preventDefault();
try {
await insertTodo({
variables: {
todo: {
name: todoName,
},
},
});
} catch (error) {
console.error(error);
return alert("Error creating todo");
}
alert("Todo created");
setTodoName("");
}}
>
<input
type="text"
placeholder="todo"
value={todoName}
onChange={(e) => setTodoName(e.target.value)}
/>
<button>Create todo</button>
</form>
</div>
{!data ? (
"no data"
) : (
<ul>
{data.todos.map((todo) => {
return <li key={todo.id}>{todo.name}</li>;
})}
</ul>
)}
</div>
);
}
export default App;
Great! We can now insert new todos. But for the new todos to be listed, we have to reload the page ourselves. We can do better, so let's fix that by introducing subscriptions.
Subscriptions updates data in realtime. When the data changes in the database, our app instantly updates and shows the newest data. It's almost like magic!
We'll replace useQuery
with useSubscription
and query
with subscription
in GET_TODOS
.
import React, { useState } from "react";
import { gql, useSubscription, useMutation } from "@apollo/client";
const GET_TODOS = gql`
subscription {
todos {
id
created_at
name
is_completed
}
}
`;
const INSERT_TODO = gql`
mutation($todo: todos_insert_input!) {
insert_todos(objects: [$todo]) {
affected_rows
}
}
`;
function App() {
const { data, loading } = useSubscription(GET_TODOS);
const [insertTodo] = useMutation(INSERT_TODO);
const [todoName, setTodoName] = useState("");
if (loading) {
return <div>Loading</div>;
}
return (
<div>
<div>
<form
onSubmit={async (e) => {
e.preventDefault();
try {
await insertTodo({
variables: {
todo: {
name: todoName,
},
},
});
} catch (error) {
alert("Error creating todo");
console.error(error);
return;
}
alert("Todo created");
setTodoName("");
}}
>
<input
type="text"
placeholder="todo"
value={todoName}
onChange={(e) => setTodoName(e.target.value)}
/>
<button>Add todo</button>
</form>
</div>
{!data ? (
"no data"
) : (
<ul>
{data.todos.map((todo) => {
return <li key={todo.id}>{todo.name}</li>;
})}
</ul>
)}
</div>
);
}
export default App;
Perfect! New todos get listed in realtime when we create them. You can even open two tabs, add new todos in one tab and see how all windows display the latest data.
Your app has superpowers!