A simple way of using React Hooks for API result caching

4 min read Original article ↗

In my web app built with the MMRN stack (s/Express/Meteor/), most pages need access to information about the currently logged on user so many of the components are composed with a withCurrentUser pattern

export const ContactUs = withCurrentUser(({ currentUser }: WithCurrentUser) => {
  return <div>...</div>;
});

My first class-based implementation was to just have my withCurrentUser wrapper make an API call and store it in state. It exposed both the API results and a method to force a reload from the server.

/**
 * Injected props
 */
export interface WithCurrentUser {
  currentUser: ExtendedProfile;
  reloadUser: () => void;
}

export const withCurrentUser = <P extends WithCurrentUser>(
  Component: React.ComponentType<P>
) => {
  return class WrappingComponent extends React.Component<
    Subtract<P, WithCurrentUser>,
    Partial<WithCurrentUser> & { cacheBuster?: number }
  > {
    state: Partial<WithCurrentUser> & { cacheBuster?: number } = {};

    fetch() {
      CurrentUserProfile.invokeAsync().then(
        currentUser => this.setState({ currentUser }),
        err => {}
      );
    }

    componentDidMount() {
      this.fetch();
    }

    componentDidUpdate(
      prevProps: Subtract<P, WithCurrentUser>,
      prevState: { cacheBuster?: number }
    ) {
      if (prevState.cacheBuster !== this.state.cacheBuster) {
        this.fetch();
      }
    }

    forceReload() {
      const { cacheBuster = 0 } = this.state;
      this.setState({ cacheBuster: cacheBuster + 1 });
    }

    render() {
      const componentProps: P = {
        ...(this.props as P),
        ...this.state,
        reloadUser: () => {
          this.forceReload();
        }
      };
      return this.state.currentUser ? <Component {...componentProps} /> : null;
    }
  };
};

This worked nicely but since each component instance would call the API, it got called quite often when moving to other pages that had other components to load the current user. Therefore I set out to create a nice hooks-based implementation based on React Contexts to cache the current user object across my application.

Now I must admit that I didn’t quite grasp how to use contexts, and when searching for ”cache API results React hooks context” I came across a few quite advanced articles that combined contexts with React dispatch/reducers and it all seemed very complicated.

Then when I saw in of the articles that the author had stored the result of useDispatch into the context I understood that it didn’t need to be so hard – I could just create my own hook and store its results in the context.

Here’s the final code which worked very fine and also exposed a few places where I need to add a reloadUser call because I had changed it:

interface ContextType {
  currentUser: ExtendedProfile | undefined;
  reloadUser: () => void;
}

/**
 * Creates a global Context to use as a provider in a root component
 * and through useContext down in children.
 * The default value is a no-op
 */
export const CurrentUserContext = React.createContext<ContextType>({
  currentUser: undefined,
  reloadUser: () => {}
});

/**
 * Returns a hook value to pass into the CurrentUserContext.Provider component.
 * Can only be used in functional components since it uses useState.
 */
export const useCurrentUserContext = (): ContextType => {
  const [currentUser, setCurrentUser] = React.useState<ExtendedProfile>();
  const [cacheBuster, setCacheBuster] = React.useState(0);

  React.useEffect(() => fetch(), [cacheBuster]);

  function fetch() {
    CurrentUserProfile.invokeAsync().then(
      currentUser => {
        setCurrentUser(currentUser);
      },
      err => {}
    );
  }

  function reloadUser() {
    setCacheBuster(prev => prev + 1);
  }

  return {
    currentUser,
    reloadUser
  };
};

export const ApiCachingContexts = ({
  children
}: React.PropsWithChildren<{}>) => {
  const context = useCurrentUserContext();
  return (
    <CurrentUserContext.Provider value={context}>
      {children}
    </CurrentUserContext.Provider>
  );
};

// in my root layout component
...
  return (
    <ApiCachingContexts>
      <MuiThemeProvider theme={getTheme(theme)}>
        // Content
...

// and finally the new implementation of withCurrentUser that had the
// same usage pattern as previously

/**
 * Injected props
 */
export interface WithCurrentUser {
  currentUser: ExtendedProfile;
  reloadUser: () => void;
}

export const withCurrentUser = <P extends WithCurrentUser>(
  Component: React.ComponentType<P>
) => {
  const WrappingComponent = (props: Subtract<P, WithCurrentUser>) => {
    const { currentUser, reloadUser } = React.useContext(CurrentUserContext);

    const componentProps: P = {
      ...(props as P),
      currentUser,
      reloadUser
    };
    return currentUser ? <Component {...componentProps} /> : null;
  };
  return WrappingComponent;
};

Post scriptum

I really ❤️ ReactJS+Typescript. The pair came as a breath of fresh air after all the horrible html/script mixin technologies I’ve been exposed to over the years (ASP with VBScript, ASP.NET classic & Razor syntax, AngularJS etc). It provides a nice functional, composable, code-only, statically typechecked development environment with tons of high-quality third-party components.