Getting Started, GraphQL & Node.js in 2023 - React urql
Tags: node, typescript, react, graphql
Published: 2023-05-31 10:03:50

### Introduction
This is part four of our tutorial series. In the previous tutorial we created a Todos GraphQL server, using `code first` approach. In this tutorial we will be creating a simple frontend using React & `urql` that will query our GraphQL server.

![todos-frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ikzwv2lz5bl9f8so4x7v.png)

### Overview
Back when I used to work with GraphQL on the client, I used to use Apollo client, and used to manually add all the types for all queries & mutations. Today we have `graphql` codegen that automatically creates types for all GraphQL operations. We will also be using `urql` as the GraphQL client library.

This series is not recommended for beginners some familiarity and experience working with Nodejs, GraphQL & Typescript is expected. In this tutorial we will cover the following :-
- Bootstrap the react project.
- Setup Codegen.
- Build the App.
- Avoid network requests, use the Cache.

All the code for this tutorial is available on [this sandbox](https://codesandbox.io/p/sandbox/yaldram-todos-urql-6ypce8).

### Step One: Bootstrap project
First create the react project, we will be using vite - 
```bash
yarn create vite todos-urql --template react-ts
```
After the react project is setup lets now install `urql` - 
```bash
yarn add urql
```
Now instantiate the `urql` client, under `main.tsx` file - 
```ts
import { createClient, Provider as UrqlProvider, cacheExchange, fetchExchange } from "urql";

const client = createClient({
  url: 'http://localhost:4000/graphql',
  exchanges: [cacheExchange, fetchExchange]
})

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <UrqlProvider value={client}>
       <App />
    </UrqlProvider>
  </React.StrictMode>
);
```
- We first create the client, and pass the `cache & fetch exchanges`, they will take care of caching our queries and re-fetching them to update our cache.
- We then wrap our App with the Provider and pass the client.

For the UI and Forms we will be using [mantine](https://mantine.dev/) - 
```bash
yarn add @mantine/core @mantine/hooks @mantine/form @emotion/react @tabler/icons-react
```
Under `main.tsx` wrap the app with MaintineProvider - 
```tsx
import { MantineProvider } from "@mantine/core"; 

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <UrqlProvider value={client}>
      <MantineProvider withGlobalStyles withNormalizeCSS>
        <App />
      </MantineProvider>
    </UrqlProvider>
  </React.StrictMode>
);
```

### Step Two: Setting up codegen
Codegen makes it easy to work with TypeScript and GraphQL, it will create all the necessary types for our queries & mutations and all our response data from queries and mutations will also be typed. From the terminal install -
```bash
yarn add -D ts-node @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
@graphql-codegen/typed-document-node
```
From the root of the project create a `codegen.yml` file - 
```yml
schema: http://localhost:4000/graphql
documents: "./src/**/*.graphql"
generates:
  ./src/graphql/generated.ts:
    plugins:
      - typescript
      - typed-document-node
      - typescript-operations
```
- We first have our schema url, so that codegen can introspect it.
- We will be writing our queries & mutations in `.graphql` files, we tell codegen to look for `.graphql` files under `src` folder.
- All the generated types will be under the `src/graphql/generated.ts` file, finally we have all the plugins.

Under `src/graphql` create a new file `todos.graphql` and add all our queries and mutations here - 
```
query GetTodos {
  todos {
    id
    task
    tags
    status
    description
    comments {
      body
      id
    }
  }
}

mutation DeleteTodo($input: DeletesTodoInput!) {
  deleteTodo(input: $input) {
    id
  }
}

mutation createTodo($input: CreateTodoInput!) {
  addTodo(input: $input) {
    id
    description
    status
    tags
    task
  }
}

mutation editTodo($input: EditTodoInput!) {
  editTodo(input: $input) {
    id
    description
    status
    tags
    task
  }
}
```

Now under `package.json` add the following scripts - 
```json
"scripts": {
    "predev": "npm run codegen",
    "codegen": "graphql-codegen",
    "dev": "vite",
    "build": "tsc && vite build",
    "preview": "vite preview"
 },
```
Whenever we run `yarn dev` the `predev` script will run and generate the types for us.

### Step Three: Building our App
Instead of creating the app in bits and pieces, I will paste the whole code here and explain - 
- We have 3 components`App.tsx`, `TodoCard` & `TodoForm`.
- In the `App.tsx` component we query for all the Todos and display them using cards in a 3-column grid. For the cards we use `TodoCard` component.
- When the user clicks on the `Create Todo` button we show the `TodoForm` component inside the Modal.
- Also, when the user clicks on the `Edit` icon inside the `TodoCard` we show the same `TodoForm` component this time with all the fields pre-filled for editing.
- Basically, we need to share the state between all these 3 components.
- For the `form state` we will be using mantine form's `createFormContext` method.
- For the Modal open close state, we will be using a global state management [legend app](https://legendapp.com/open-source/legend-state-v1/).

First from the terminal install - 
```ts
yarn add @legendapp/state
```

Under `src/store.ts` paste the following - 
```ts
import { observable } from "@legendapp/state";
import { createFormContext } from "@mantine/form";

import { Todo } from "./graphql/generated";

export const store = observable({ showForm: false, isEditing: false });

export const [FormProvider, useFormContext, useForm] = createFormContext<Omit<Todo, "__typename" | "comments">>();
```
- We created 2 pieces of state, one `showForm` to open and close the form modal.
- Next `isEditing` to know whether we are editing a Todo so that we can change the Modal title from create to edit and also fire the correct mutation when we submit the form.
- Finally, we create a formContext to share the form state between the `App` & `TodoForm` component.

Now under `src/TodoCard.tsx` paste the following - 
```tsx
import { IconEdit, IconTrash } from "@tabler/icons-react";
import {
  Card,
  Stack,
  Group,
  Text,
  ActionIcon,
  Badge
} from "@mantine/core"

import { TaskStatus, Todo } from "./graphql/generated";

type TodoCardProps = {
  todo: Todo
  onEditTodo: (todo: Todo) => void;
  onDeleteTodo: (todoId: string) => void;
}

export function TodoCard({ todo, onEditTodo, onDeleteTodo }: TodoCardProps) {
  return (
    <Card h="100%" shadow="sm" p="lg" radius="md" withBorder>
      <Stack key={todo.id}>
        <Stack>
          <Group position="apart">
            <Text size="lg" weight={500}>
              {todo.task}
            </Text>
            <Group>
              <ActionIcon onClick={() => onEditTodo(todo)}>
                <IconEdit color="blue" size={16} />
              </ActionIcon>
              <ActionIcon onClick={() => onDeleteTodo(todo.id)}>
                <IconTrash color="red" size={16} />
              </ActionIcon>
            </Group>
          </Group>
          <Group>
            <Badge color={getStatusColor(todo.status)}>
              {todo.status}
            </Badge>
          </Group>
        </Stack>
        <Text size="sm" color="dimmed">
          {todo.description}
        </Text>
        <Group mt="sm">
          {todo.tags?.map((tag, index) => (
            <Badge key={index}>{tag}</Badge>
          ))}
        </Group>
      </Stack>
    </Card>

  )
}

function getStatusColor(status: TaskStatus) {
  if (status === TaskStatus.Pending) {
    return "red";
  } else if (status === TaskStatus.InProgress) {
    return "grape";
  } else {
    return "green";
  }
}
```

Similarly, under `src/TodoForm.tsx` paste the following - 
```tsx
import { useState } from "react"
import { useMutation } from "urql";
import { observer } from "@legendapp/state/react"
import {
  Group,
  Stack,
  Modal,
  Button,
  LoadingOverlay,
  TextInput,
  Textarea,
  Select,
  MultiSelect,
} from "@mantine/core";

import { store, useFormContext } from "./store"
import {
  TaskStatus,
  CreateTodoDocument,
  EditTodoDocument,
} from "./graphql/generated";

function Component() {
  const form = useFormContext();
  const [tags, setTags] = useState(["GraphQL", "Pothos"]);

  const [{ fetching: creatingTodo, }, createTodo] = useMutation(CreateTodoDocument);
  const [{ fetching: updatingTodo }, editTodo] = useMutation(EditTodoDocument);

  return (
    <Modal
      centered
      opened={store.showForm.get()}
      onClose={() => store.showForm.toggle()}
      title={`${store.isEditing.get() ? "Edit task" : "Create a new task"}`}
    >
      <form onSubmit={form.onSubmit(async (values) => {
        const task = {
          task: values.task,
          description: values.description,
          status: values.status,
          tags: values.tags,
        };
        try {
          if (store.isEditing.get()) {
            await editTodo({ input: { id: values.id, ...task } });
          } else {
            await createTodo({ input: task });
          }
        } catch (error) {
          console.log("onSubmit Error", error);
        } finally {
          store.showForm.set(false);
          store.isEditing.set(false)
          form.reset();
        }
      }
      )}>
        <LoadingOverlay visible={creatingTodo || updatingTodo} />
        <Stack spacing="sm">
          <TextInput
            autoFocus
            required
            placeholder="Learn GraphQL"
            label="Task"
            {...form.getInputProps("task")}
          />
          <Textarea
            required
            placeholder="Learn Code first GraphQL"
            label="Task Description"
            {...form.getInputProps("description")}
          />
          <Select
            label="Task Status"
            placeholder="Pick one"
            data={[
              TaskStatus.Pending,
              TaskStatus.InProgress,
              TaskStatus.Done,
            ]}
            {...form.getInputProps("status")}
          />
          <MultiSelect
            label="Tags"
            data={tags}
            placeholder="Select items"
            searchable
            creatable
            getCreateLabel={(query) => `+ Create ${query}`}
            onCreate={(query) => {
              setTags((current) => [...current, query]);
              return query;
            }}
            {...form.getInputProps("tags")}
          />
          <Group position="right">
            <Button w="150px" color="green" type="submit">
              {store.isEditing.get() ? "Edit" : "Create"}
            </Button>
          </Group>
        </Stack>
      </form>
    </Modal>
  )
}

export const TodoForm = observer(Component)
```
- Take a note that we are wrapping our component with the observer function, so legend state will re-render it whenever the global state changes.

Finally under the `src/App.tsx` paste the following - 
```tsx
import { useCallback } from "react"
import { useQuery, useMutation } from "urql";
import {
  Group,
  Grid,
  Loader,
  Stack,
  Button,
  Box,
  Text
} from "@mantine/core";

import { TaskStatus, Todo, DeleteTodoDocument, GetTodosDocument } from "./graphql/generated";
import { TodoForm } from "./TodoForm";
import { TodoCard } from "./TodoCard";
import { store, useForm, FormProvider } from "./store";

export function App() {
  const [{ fetching, data, error }] = useQuery({
    query: GetTodosDocument
  })

  const [_, deleteTodo] = useMutation(DeleteTodoDocument);

  const form = useForm({
    initialValues: {
      id: "",
      task: "",
      tags: [] as string[],
      status: TaskStatus.Pending,
      description: "",
    },
  });

  const onEditTodo = useCallback((todo: Todo) => {
    store.isEditing.set(true);
    form.setFieldValue("task", todo.task);
    form.setFieldValue("description", todo.description);
    form.setFieldValue("status", todo.status);
    form.setFieldValue("tags", todo.tags ?? []);
    form.setFieldValue("id", todo.id);
    store.showForm.set(true)
  }, [])

  const onDeleteTodo = useCallback((todoId: string) => {
    deleteTodo({ input: { id: todoId } })
  }, [])

  if (fetching) {
    return (
      <Stack h="100vh" align="center" justify="center">
        <Loader variant="bars" />
      </Stack>
    );
  }

  if (error) return <div>Error....</div>;

  return (
    <FormProvider form={form}>
      <Box p="md">
        <TodoForm />

        <Group position="right">
          <Button onClick={() => store.showForm.set(true)}>Create a new task</Button>
        </Group>

        {data?.todos.length !== 0 ? (
          <Grid p="lg">
            {data?.todos.map((todo) => (
              <Grid.Col key={todo.id} span={4}>
                <TodoCard
                  todo={todo}
                  onEditTodo={onEditTodo}
                  onDeleteTodo={onDeleteTodo}
                />
              </Grid.Col>
            ))}
          </Grid>
        ) : (
          <Stack h="100vh" align="center" justify="center">
            <Text weight={500}>You don't have any tasks!</Text>
          </Stack>
        )}
      </Box>
    </FormProvider>
  );
}
```
- We wrap the whole App with the `Form Provider`.

One question may arise like why are we handling the onEdit and onDelete in the main app and not the TodoCard ? 
- Given the fact that we are wrapping the whole app inside the FormContext whenever we type in the form fields all the components that are using the form context will re-render.
- So, if we use `const form = useFormContext()` in the `TodoCard` it will also re-render when we interact with the form.
- Therefore, I handled all the form related logic in the `App & TodoForm` component and using `useCallback` made sure that the `onEditTodo & onDeleteTodo` do not get re-created on every app render, this prevents the `TodoCard` from unnecessary re-renders.

Now run the app using `yarn dev` and play with it.

### Step Four: Caching 
You might have noticed one thing, when we edit or delete a Todo it is reflected on the UI, but when we create a new Todo it does not show. Also, open the network tab and you will see that `urql` is refetching the todos after every mutation.

I don't want this, I want to update the local cache after the mutation, I don't want un-necessary network requests. For this we need to use `@urql/exchange-graphcache` library - 
```bash
yarn add @urql/exchange-graphcache 
```
Under `main.tsx` paste the following - 
```tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { MantineProvider } from "@mantine/core";
import { createClient, Provider as UrqlProvider, fetchExchange } from "urql";
import { cacheExchange } from "@urql/exchange-graphcache";

import { App } from "./App";
import { GetTodosDocument, Todo } from "./graphql/generated";

const cache = cacheExchange({
  updates: {
    Mutation: {
      addTodo(result, _args, cache) {
        cache.updateQuery({ query: GetTodosDocument }, (data) => {
            data?.todos?.push({...result?.addTodo as Todo, comments: []});
            return data;
        })
      },
      deleteTodo(result: { deleteTodo: { id: string } }, _args, cache) {
        cache.updateQuery({ query: GetTodosDocument }, (data) => {
          return {
            ...data,
            todos: data?.todos?.filter((todo: Todo) => todo.id !== result?.deleteTodo?.id) || []
          }
        })
      }
    }
  }
})

const client = createClient({
  url: 'http://localhost:4000/graphql',
  exchanges: [cache, fetchExchange]
})

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <UrqlProvider value={client}>
      <MantineProvider withGlobalStyles withNormalizeCSS>
        <App />
      </MantineProvider>
    </UrqlProvider>
  </React.StrictMode>
);
```
- We are listening to the `addTodo & deleteTodo` mutations, and whenever we have a successful mutation, we will add the data to the `GetTodos` query cache and it will be reflected on the UI with no network requests.
- Finally, we remove the `urql` cacheExchange function from the exchanges array and pass our cache to it.

Run the project, open the network tab and check if our cache is working as expected for each mutation there should not be any network request for `todos query` and the UI should also be updated.

### Conclusion
In this tutorial we build a simple frontend for our GraphQL backend using the latest tools available. We used codegen to avoid creating types manually, also we used the latest atomic state management library `legendapp` instead of passing props all over. Finally, we made use of the local cache provided by `urql` and avoided unnecessary network requests to the server. Now that I have explored GraphQL with Node.js my next goal is to try out [trpc](https://trpc.io/) and compare it with GraphQL. Until next time PEACE.