Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Setting App State with React Query

April 07, 2021
reacttypescript

This is another post in a series of posts using React Query with TypeScript.

Previous posts:

This post will cover two approaches to set app level state from a web service request with React Query. The app level state is stored in a React context.

App state

Query client provider

React Query requires a QueryClientProvider component above the components using it:

import { QueryClient, QueryClientProvider } from "react-query";
...

const queryClient = new QueryClient();
render(
  <QueryClientProvider client={queryClient}>
    <App />
  </QueryClientProvider>,
  rootElement
);

We’ve added QueryClientProvider at the top of the component tree to make React Query available to any component.

App data context

Our app level state is going to be in a React context. Here is its type and creation:

// AppDataProvider.tsx
export type User = {
  name: string;
};
type AppDataType = {
  user: User | undefined;
  setUser: (user: User) => void;
};
const AppData = React.createContext<AppDataType>(undefined!);

The context contains information about a user in a user property. We are only storing the user’s name in this example. The context also has a function, setUser, which consumers can use to set the user object.

We pass undefined into createContext and use a non-null assertion (!) after it. This is so that we don’t have to unnecessarily check for undefined in consuming code that interacts with the context.

Here is a provider component for this context:

// AppDataProvider.tsx
export function AppDataProvider({ children }: { children: React.ReactNode }) {
  const [user, setUser] = React.useState<User | undefined>(undefined);
  return (
    <AppData.Provider value={{ user, setUser }}>{children}</AppData.Provider>
  );
}

This component contains the user state and makes it available to consumers of the context.

Here is a hook that consumers can use to access the context:

// AppDataProvider.tsx
export const useAppData = () => React.useContext(AppData);

We can now use the provider component high in the component tree:

import { AppDataProvider } from "./AppDataContext";...
render(
  <QueryClientProvider client={queryClient}>
    <AppDataProvider>      <App />
    </AppDataProvider>  </QueryClientProvider>,
  rootElement
);

Fetching function

Our fetching function for React Query takes in a query key. In this example, this is a tuple containing the resource name and parameters to get a user record.

type Params = {
  queryKey: [string, { id: number }];
};
async function getUser(params: Params) {
  const [, { id }] = params.queryKey;
  const response = await fetch(`https://swapi.dev/api/people/${id}/`);
  if (!response.ok) {
    throw new Error("Problem fetching user");
  }
  const user = await response.json();
  assertIsUser(user);

  return user;
}

We are pretending the Star Wars API contains our users.

fetch is used to make the request. An error is thrown if the response isn’t successful. React Query will manage the error for us, setting the status state to "error" and error to the Error object raised.

The fetching function is expected to return the response data that we want to use in our component. We use a type assert function called assertIsUser to ensure the data is correctly typed:

import { User } from "./AppDataContext";
...
function assertIsUser(user: any): asserts user is User {
  if (!("name" in user)) {
    throw new Error("Not user");
  }
}

Query

We will use React Query inside a component to fetch the user data and set it in the app state.

export function App() {
  const { status, error } = useQuery<User, Error>(
    ["user", { id: 1 }],
    getUser
  );

  if (status === "loading") {
    return <div>...</div>;
  }
  if (status === "error") {
    return <div>{error!.message}</div>;
  }

  return <Header />;
}

We use the useQuery hook from React Query to execute the getUser fetching function and pass in a user id of 1 for it to fetch.

We use the status state variable to render various elements in the different fetching states. The Header component is rendered after the data has been fetched. We’ll look at the Header component a little later.

Setting app data from the query

We need to set the user data fetched in the app data context. Let’s start by getting access to the setter function using the useAppData hook:

import { useAppData, User } from "./AppDataContext";...
export function App() {
  const { setUser } = useAppData();  ...
}

There is an onSuccess function that can be executed after React Query has successfully executed the fetching function to get data. We can use this to update the app data context.

export function App() {
  const { setUser } = useAppData();

  const { status, error } = useQuery<User, Error>(
    ["user", { id: 1 }],
    getUser,
    { onSuccess: (data) => setUser(data) }  );
}

The onSuccess function takes in the data that has been fetched, so we pass this to the setUser function.

Rendering a header with app data

Lower level components can now access app data context. Here is the Header component displaying the user name:

import { useAppData } from "./AppDataContext";

export function Header() {
  const { user } = useAppData();
  return user ? <header>{user.name}</header> : null;
}

The code for this example is available in CodeSandbox at https://codesandbox.io/s/react-query-app-state-ud6mz?file=/src/App.tsx

Moving query to AppDataProvider

This is nice and shows how we can push state from React Query into other state providers. However, we can simplify this example by using the state in React Query and removing our own state.

Let’s start by moving the React Query to be inside AppDataProvider:

// AppDataContext.tsx
...
import { useQuery } from "react-query";
...
type AppDataType = {
  user: User | undefined;
};
...
export function AppDataProvider({ children }: { children: React.ReactNode }) {
  const { data: user } = useQuery<User, Error>(["user", { id: 1 }], getUser);

  return <AppData.Provider value={{ user }}>{children}</AppData.Provider>;
}
...

We end up removing a fair bit of code:

  • We’ve removed the setUser function from context.
  • We’ve removed the useState from the context provider.
  • We’ve removed the destructured status and error state variables from the query. Instead, we have destructured the data state variable and aliased this as user.
  • We’ve removed the onSuccess function in the query.

The consuming code in the Header component remains the same.

Nice. 😊

The code in this second example is available in CodeSandbox at https://codesandbox.io/s/react-query-app-state-2-4he1q?file=/src/AppDataContext.tsx

Did you find this post useful?

Let me know by sharing it on Twitter.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Find out more