Setting App State with React Query
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.
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
anderror
state variables from the query. Instead, we have destructured thedata
state variable and aliased this asuser
. - 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
If you to learn more about using TypeScript with React, you may find my course useful: