GraphQL β€’ 5 min read

Managing Data and State: React Hooks, Recoil and Apollo GraphQL Client

Managing Data and State: React Hooks, Recoil and Apollo GraphQL Client

State and data management have proven to be complicated endeavors over time as React has evolved as a library. From the good old fashioned, built-in setState method of class based components, to a composition mindset brought to us by libraries like Recompose (remember that one?) - we've had enough to keep our plates full over the years... and then some. High-Order Components, render props, global state, actions, reducers, memoization, side effects... the list goes on and on when we take a trip down memory lane. All of this to accomplish one main thing - UI data and state management. React Hooks have standardized a bit of this, specifically when it comes to local state management. In this post, I'll illustrate some real world patterns I've established in building the new www.foo.software.

App Loading State with Custom React Hooks and Recoil

I decided to establish an app level "loading" state as a dictionary-like data structure (example below). Each line represents a part of the app in a loading state. When all parts have completed loading, the object will be empty.

{
  "userFetch": "userFetch",
  "someOtherDataFetch": "someOtherDataFetch"
}

With the above in mind, I created a Recoil "atom" to hold this "loading" piece of global state.

An atom represents a piece of state. Atoms can be read from and written to from any component. Components that read the value of an atom are implicitly subscribed to that atom, so any atom updates will result in a re-render of all components subscribed to that atom

~ Getting Started | Recoil

// state/loadingState.ts
import { atom } from 'recoil';

export type LoadingCollectionType = Record<string, string>;

export const loadingState = atom({
  key: 'loadingState',
  default: {} as LoadingCollectionType,
});

Combining useEffect React Hook with useRecoilState

Recoil offers many benefits - one being its compatibility and adoption of hooks. The library provides a useRecoilState hook that is pretty robust in that it supports selectors (which I won't get into in this post). Below you can see how I use the hook to set and remove loading state based on id (being the key of the key / value pair collection encompassing my loading state).

// state/useLoading.ts
import { useEffect } from 'react';
import { useSetRecoilState } from 'recoil';
import { loadingState } from './loadingState';

export default function useLoading({
  id,
  shouldShowLoading,
}: {
  id: string;
  shouldShowLoading: boolean;
}) {
  const setLoading = useSetRecoilState(loadingState);

  useEffect(() => {
    const removeLoadingState = () => {
      setLoading((oldLoading) => {
        const { [id]: _removed, ...updatedLoading } = oldLoading;
        return updatedLoading;
      });
    };

    if (shouldShowLoading) {
      setLoading((oldLoading) => ({
        ...oldLoading,
        [id]: id,
      }));
    } else {
      removeLoadingState();
    }

    // if component unmounts while in loading state,we need to make
    // sure we cleanup with `useEffect` unsubscribe (end loading state)
    return () => {
      removeLoadingState();
    };
  }, [shouldShowLoading]);
};

In the above example, I add a key / value pair to the loading state when shouldShowLoading is true, otherwise I remove it. With the above, whenever the shouldShowLoading parameter changes so does loading state for the piece identified by the id parameter.

Next, I consume the app loading state naturally in my App component, which ends up looking something like this.

// components/App.tsx
import LinearProgress from './LinearProgress';
import { loadingState } from '../state/loadingState';

export default function App({ children }: { children: ReactElement }) {
  const [loading] = useRecoilState(loadingState);
  return (
    <>
      {children}
      {!Object.keys(loading).length ? null : <LinearProgress />}
    </>
  );
}

With the above, we show a linear "progress bar" component at the top of the page. This global loading state is key in that we can use it to show the progress bar at the page level and / or as the user is navigating in-between pages.

App level progress bar component

You saw how I created and used an app loading state to display a progress bar, but we're missing a piece. How do we set this loading state? This is where the real fun begins, hold on tight πŸ‡

Data Fetching with Apollo GraphQL Client

Many projects I've worked on utilize a combination of Redux and some flavor of "side effect" management with libraries like Redux-Saga, RxJS or something else (of course the list goes on and on... of course). When rebuilding Foo, I decided to do everything in my power to avoid the Redux song and dance altogether and found it was easier than I thought! In working with these projects, I noticed a naturally established pattern of exclusively harnessing the side effect (Sagas, etc) + Redux combo for data fetching and persistence. This strategy boxes us into a model of unnecessarily populating app level state with data that might only be used in a part of an application. It also insists that we create loads of boilerplate whenever we add a new remote data source. With the help of Apollo's GraphQL client I was able to avoid all that and populate global state with data actually only used globally.

Colocating GraphQL Queries and Mutations with Custom React Hooks

One common pattern in a React architecture with GraphQL is to colocate queries and mutations with component code (in the same file or directory). I think this a good pattern, but can become complicated and downright impossible when needed across multiple components and pages. I prefer colocation with custom hooks. In the below example I created a custom hook to fetch data for the logged in user.

// state/useAccountUser.ts
import { gql, useQuery } from '@apollo/client';
import UserInterface from '../interfaces/User';
import useLoading from './useLoading';

const USER_QUERY = gql`
  query user($version: String) {
    user {
      email
      name
    }
  }
`;

export default function useAccountUser({
  loadingId,
}: {
  loadingId: string;
}) {
  const { data, error, loading, refetch } = useQuery<
    { user: UserInterface }
  >(USER_QUERY);

  useLoading({
    id: loadingId,
    shouldShowLoading: loading,
  });
 
  return { data, error, loading, refetch };
};

export default useAccountUser;

With the above, in any component, I can use this hook to get user data, including loading state. And as you may have noticed, we put our useLoading hook to work to display a progress bar whenever user data is being loaded. This answers the earlier question about how we set a piece of the global loading state - by passing id and shouldShowLoading parameters to the useLoading hook as seen above. One of the best parts about this is that I have no dependency on global state to get  user data 😎 You might be wondering - how is that?! The answer lies in core behavior of Apollo Client.

Caching in Apollo Client

Apollo Client provides caching out of the box in a local, normalized, in-memory cache. The useQuery hook will automatically cache queries of the same input by default. Without this I may have been sucked into the familiar Redux + side effect management patterns just to persist remote data. No thank you, I'll pass πŸ’…

In any component that depends on user data, I can simply use my custom hook as seen below. I don't need to worry about actions, reducers, sagas, selectors or whatever else the kids are doing these days. If I use this hook in multiple components rendered in the same page load, I can rest assured the remote fetch will only initiate once, thanks to Apollo Client caching!

// components/MyComponent.tsx
import useAccountUser from '../state/useAccountUser';

export default function MyComponent() {
  const { data, loading } = useAccountUser({
    loadingId: 'myComponentUser',
  });
  
  if (loading) {
    // your loading state would probably be better than this :D
    return null;
  }
  
  return <>Hello {data.user.name}!</>
};

Conclusion

Hopefully in the last section above, you can see how all the pieces of this post come together. In summary this post illustrates an app level "loading state" to show a progress bar loader component (also at the app level) displayed while fetching user data to be used by any component. There are many benefits of utilizing Recoil and Apollo Client in the way described in this post, primarily moving away from a limited mindset of binding remotely fetched data to app level state.