Skip to content

Mythica: React app with tRPC Server and ReactQuery

Posted on:May 27, 2023 at 07:00 PM

image

This series of articles focuses on building a full-stack app with the following technology stack: PlanetScale - Prisma - tRPC - React. The project name is mythica and it will allow users to collect random mythical creatures. Here is the full list of the articles:

Table of contents

Open Table of contents

Starting React Project

1. Build Tool

I will be using vite to build the React application, although, this is not mandatory so feel free to use any build tool you prefer. So simply re-create the client directory by starting a new React-typescript project inside the packages folder.

npm create vite@latest client --template react-ts

2. Dependancies

After vite creates the app, make sure to install the necessary dependencies. Your dependencies section in your client’s package.json should be similar to this.

"dependencies": {
    "@emotion/react": "^11.11.0",
    "@mantine/core": "^6.0.11",
    "@mantine/hooks": "^6.0.11",
    "@tabler/icons-react": "^2.20.0",
    "@tanstack/react-query": "^4.29.7",
    "@trpc/client": "^10.27.1",
    "@trpc/react-query": "^10.27.1",
    "@trpc/server": "^10.27.1",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-query": "^3.39.3",
    "server": "^1.0.0",
    "zod": "^3.21.4"
  }

Please note that the mandatory ones are @tanstack/react-query, @trpc/client, @trpc/react-query, server and zod. I use mantine as the UI library with TablerIcons but feel free to use whatever you prefer. It is important to notice that server dependancy is indeed the local server package we have in the monorepo. You can simply install it by running the following: npm i server —workspace=client.

App Setup

1. Create a tRPC client

Inside src create a new directory lib which will contain a single file trpc.ts which will create a trpc instance to React that exposes the AppRouter from our server package we created earlier in one of the previous articles.

import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "server";

export const trpc = createTRPCReact<AppRouter>();

We will start by replacing the code we have in App.tsx with the following, and we will explain it later:

import { useState } from "react";
import { trpc } from "./lib/trpc";
import "./App.css";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { httpBatchLink } from "@trpc/react-query";
import { RandomCreature } from "./Pages/RandomCreature";
import {
  ColorScheme,
  ColorSchemeProvider,
  Flex,
  MantineProvider,
} from "@mantine/core";
import { SwitchToggle } from "./Components";

const BACKEND_URL = "http://localhost:3000/mythica";

function App() {
  const [queryClient] = useState(() => new QueryClient());
  const [trpcClient] = useState(() =>
    trpc.createClient({
      links: [
        httpBatchLink({
          url: BACKEND_URL,
        }),
      ],
    })
  );
  const [colorScheme, setColorScheme] = useState<ColorScheme>("light");
  const toggleColorScheme = (value?: ColorScheme) =>
    setColorScheme(value || (colorScheme === "dark" ? "light" : "dark"));

  return (
    <MantineProvider theme={{ colorScheme }} withGlobalStyles withNormalizeCSS>
      <ColorSchemeProvider
        colorScheme={colorScheme}
        toggleColorScheme={toggleColorScheme}
      >
        <trpc.Provider queryClient={queryClient} client={trpcClient}>
          <QueryClientProvider client={queryClient}>
            <Flex
              mih={50}
              my={-500}
              gap="lg"
              align="start"
              justify="space-between"
              direction="column"
              wrap="wrap"
              mb={100}
            >
              <SwitchToggle />
              <RandomCreature />
            </Flex>
          </QueryClientProvider>
        </trpc.Provider>
      </ColorSchemeProvider>
    </MantineProvider>
  );
}

export default App;

We will ignore theme-related stuff. The important bits here are that we obtain a QueryClient from react-query and start a trpc client providing it with our server url. Later, when we return the JSX we wrap the entire app with trpc.Provider and QueryClientProvider; this way we can execute the procedures we have on the server from any component in our React app. Notice how the linkage is done between the two. trpc.Provider takes in queryClient from react-query. This way when we use our trpc client from any component we will get the benefits of react-query refetch and caching API.

<trpc.Provider queryClient={queryClient} client={trpcClient}>
  <QueryClientProvider client={queryClient}>...</QueryClientProvider>
</trpc.Provider>

Using getRandom and getDetails procedures from React

In the previous article we implemented two procedures: getRandom and getDetails. The goal here is that users will be presented with random creatures each time they visit the application and can view more details on this random creature by hovering over a button. Now we will create two React components: RandomCreature and CreatureDetails which will consume each of the mentioned procedures, respectively.

1. RandomCreature Component

The entire code for this component can be found here, the following is a simplified version to showcase the important bits that relates to this article

export function RandomCreature() {
  const response = trpc.creature.getRandom.useQuery();

  const { classes } = useStyles();

  if (response.isLoading || response.isRefetching) {
    return <AppLoader />;
  }
  if (response.isError || !response.data?.creature) {
    return <AsyncError />;
  }

  const creature = response.data?.creature;

  return <>... // build your ui in case there is data</>;
}

As you can see, it is that simple to load data from a procedure inside a component with trpc and react-query. Notice that we have create AppLoader and AsyncError under the Components folder to view an appropriate UI depending in the response. We also provide our AppLoader as fallback value which will be displayed until the data loads, or when we try to re-fetch the data by clicking the skip icon, but this bit will be explained in details in the next article.

2. CreatureDetails Component

import {
  HoverCard,
  Avatar,
  Text,
  Group,
  Anchor,
  Stack,
  Button,
  Center,
  Badge,
} from '@mantine/core';
import { trpc } from '../lib/trpc';
import { AppLoader, AsyncError } from '../Components';

interface CreatureDetailsProps {
  id: number;
}
export function CreatureDetails(props: CreatureDetailsProps) {
  const { id } = props;
  const response = trpc.creature.getDetails.useQuery({ id: id });
  const details = response?.data;
  if (response.isLoading) {
    return <AppLoader />;
  }
  if (response.isError || !details) {
    return <AsyncError />;
  }

  return (
   // jsx
  );
}

It is worth mentioning here, the only difference in this component is that it depends on the id from the currently displayed random creature as we can see from the function argument. We also do not need to do refetching here per our game logic which will be explained in details in the next article, so that is why we omit response.isRefetching.

Similarly, CreatureDetails component uses the same approach. How

Conclusion

In this tutorial, we have learned how to consume trpc procedures from within a React application while using react-query caching and refecthing capabilities. Next step, we will be describing our game logic as users will be able to collect creatures in a certain way in order to win the game.

You can find the final code for this article here