--- tags: library, Protocol Docs, product docs --- # Frontend Data Structure and Flow We've been in the process of refactoring our frontend data structure and flow. Previously, we've been storing all of our data calls in the `UserContext` which became difficult to maintain and reason around. We've been refactoring to leverage [React Query](https://tanstack.com/query/v4/?from=reactQueryV3&original=https://react-query-v3.tanstack.com/) and move to individual hooks for all of our data. Previously, each call in the `UserContext` used our SDK and passed in the arguments directly into the SDK function. Since our SDK is Promise based, and React Query works with any query or mutation that is Promise based, we're able to create hooks that retain a similar relationship with the SDK. Each new hook wraps either `useQuery` (queries) or `useMutation` (mutations) and integrate the relevant SDK calls. With queries, we pass the arguments that the associated SDK call takes into the hook which then passes them into the SDK call. Each hook is then only responsible for a single piece of data (query or mutation) and we can import and call them in React components where needed. This allows for much more controlled, maintainable code that is more direct to reason about and understand. We can then only call the hooks that we need from the components that directly need them instead of sprawling context calls which evolved to include a lot of side effects from the chains of fetching and storing data in a massive context and provider structure. ## Benefits of React Query React Query (RQ) contains many data fetching best practices that we're able to leverage. Additionally, since our hooks wrap a structure already provided (and battle tested) by React Query our data flow is cleaner and more direct to problem solve. RQ also contains several helpers for managing states with asynchronous queries and mutations. Each query has data, success, error, and loading states provided. We can then return these in our hook and tap into them in the UI. The mutations contain similar helpers. ## Hook Structure All of the new data hooks are in the `protocol-frontend` in the `hooks` folder. The names follow a standard hooks convention of `useNounVerb` where each of the nouns correspond to our core data elements and the verbs are from the SDK (read: get, list and write: create, update, delete). All of our data hooks use this convention, such as `useDaoList` or `useContributionList` and `useContributionCreate`. Our `UserContext` can then be used solely for global state that truly needs to live across the entire app such as the `userData` and the `GovrnProtocol` initialization. ## React Query Provider and Client We initialize our React Query `queryClient` and configure it to support global error handling for our queries with a global Toast. ```jsx // main.tsx const queryClient = new QueryClient({ queryCache: new QueryCache({ onError: error => { toast({ title: 'Something went wrong.', description: `Please try again: ${error}`, status: 'error', duration: 3000, isClosable: true, position: 'top-right', }); }, }) ``` This taps into all of our cached queries and listens for their `onError` callback and then pops up a global Toast displaying the query error. Once the client is configured we wrap our app in the React Query context provider `<QueryClientProvider client={queryClient} />` above our `<Routes/>` component in our `App` -- this is in the frontend's `main.tsx` component. ### Queries Each query requires a unique key. This key is used for the caching and also for invalidating the current cache. This invalidation allows RQ to refetch in the background after a mutation. This enables a smooth UX where new data is shown without needing to do a page refresh. Let's look at the `useDaoList` hook: ``` // useDaoList.ts import { useUser } from '../contexts/UserContext'; import { useQuery } from '@tanstack/react-query'; import { UIGuilds } from '@govrn/ui-types'; export const useDaosList = ({ ...args }) => { const { govrnProtocol: govrn } = useUser() const { isLoading, isFetching, isError, error, data } = useQuery(['userDaos', args], async () => { const data = await govrn.guild.list({ ...args }) return data }) return { isLoading, isError, isFetching, error, data }; } ``` The hook can receive args that are then spread into the SDK call. This allows for us to use the hook in flexible ways such as: ```jsx const { isLoading: daosListIsLoading, isError: daosListIsError, data: daosListData, } = useDaosList({ where: { users: { some: { user_id: { equals: userData?.id } } } }, // show only user's DAOs }); ``` and ```jsx const { isLoading: daosListIsLoading, isError: daosListIsError, data: daosListData, } = useDaosList({ }); // show all DAOs since no args are passed in ``` We bring in the `govrnProtocol` initialization from `UserContext` so that we're able to use the SDK calls. We then use `useQuery` and can destructure out the specific states: ``` const { isLoading, isFetching, isError, error, data } = useQuery(['userDaos', args], async () => { const data = await govrn.guild.list({ ...args }) return data }) ``` `useQuery` takes in an array of arguments, the first of which is the query's key. This needs to be unique and is what'll be used for invalidation. In this example, the query's name is `userDaos` and we're also passing in the args as part of the key array. This allows for each specific query using different `args` to have it's own cache. When invalidating we can reference the name without the args, such as `userDaos`. The hook then returns all of the states coming from `useQuery`, which we can then use in the frontend. ### Mutations Our mutation hook structure involves more code but utilizes a similar pattern to the queries. We wrap `useMutation` from RQ, utilize the corresponding call from the SDK passing in any args, and return the states and data. We handle any errors via the `onError` callback and the function with the `onSuccess` function. We can choose which queries we want to invalidate after a successful mutation. RQ will then do a background refetch for data in any of the invalidated queries, which creates a smooth UX for users interacting with data reactive components. Let's look at the `useContributionCreate` hook. The mutation hooks have more code than the query hooks but the structure is still the same. Since we're utilizing asynchronous calls, we use `useMutateAsync` rather than `useMutate`. ```jsx import { useUser } from '../contexts/UserContext'; import { useToast } from '@chakra-ui/react'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ContributionFormValues } from '../types/forms'; export const useContributionCreate = () => { const toast = useToast() const { govrnProtocol: govrn, userData } = useUser() const queryClient = useQueryClient() const { mutateAsync, isLoading, isError, isSuccess } = useMutation(async (newContribution: ContributionFormValues) => { const data = await govrn.custom.createUserContribution({ address: userData?.address ?? '', chainName: 'ethereum', userId: userData?.id ?? -1, name: newContribution.name || '', details: newContribution.details || '', proof: newContribution.proof || '', activityTypeName: newContribution.activityType || '', dateOfEngagement: new Date(newContribution.engagementDate || '').toISOString(), status: 'staging', guildId: Number(newContribution.daoId) || undefined, }) return data }, { onSuccess: () => { queryClient.invalidateQueries(['activityTypes']) toast({ title: 'Contribution Report Added', description: 'Your Contribution report has been recorded. Add another Contribution report or check out your Contributions.', status: 'success', duration: 3000, isClosable: true, position: 'top-right', }); }, onError: (error) => { toast({ title: 'Unable to Report Contribution', description: `Something went wrong. Please try again: ${error}`, status: 'error', duration: 3000, isClosable: true, position: 'top-right', }); }, } ) return { mutateAsync, isLoading, isError, isSuccess } } ``` We pull in the `queryClient` and call `queryClient.invalidateQueries()` passing in the keys for any queries we want to invalidate. We'll be adding additional queries to this invalidation list as we continue to refactor. Invoking the invalidation on a successful mutation triggers RQ to invalidate the current cache for each of the query keys, causing a background refetch. The user won't need to do any page refresh -- the UI will react to the fresh data. If any query takes a sufficiently long time the user may see a loading spinner in the UI again from the `isLoading` status from the query, but otherwise there is no need to even leave the component that they're in. One usecase for this is if a user has selected to 'Create More' Contributions after initially creating one. If they were to add a new activity type through the create mutation, the new activity type would refetch because of the invalidation of the `activityTypes` query and the newly added activity type will be available without the user needing to even close the form. This is a powerful pattern that we'll be able to leverage for other mutations and queries.