Introducing RTK-Query-Loader

Articles|Håkon Underbakke | about 1 year ago

A new package that I’ve made.

What problem does it solve?

Let us say that you are using redux toolkit, and utilising createAPI and it’s generated useQuery hooks. You might have found yourself in a situation similar to this:

  • I have a component that needs data from multiple queries

And depending on your preferences, you might have gone with a solution like this (early returns):

const Component = () => {
   const query1 = useQuery1();
   const query2 = useQuery2();
   
   if (query1.isLoading || query2.isLoading) return <Loader />;
   if (query1.isError || query2.isError) return <Error />;
   //...
   // Down here you can safely assume you have all required data.
   return <div>{query1.data.name} VS {query2.data.name}</div>
}

Or maybe even something like this:

const Component = () => {
   const query1 = useQuery1();
   const query2 = useQuery2();

   // Using optional chaining and fallbacks everywhere
   return (
      <>
         {query1.data?.name && query2.data?.name 
            ? (<div>{query1.data.name} VS {query2.data.name}</div>) 
            : <Loading />
         }
      </>
}

This is fine for simple small components, but it adds a bit of mental overhead when reading the code. Imagine you could do something like this instead:

export const Component = withLoader(
  (props: Props, loaderData) => {
    // Normal component, just with preloaded data
    return <div>{loaderData.player1.name} VS {loaderData.player2.name}</div>
  },
  multiplayerLoader
);

// somewhere else
export const multiplayerLoader = createLoader({
   queries: () => [useGetPlayerOneData(), useGetPlayerTwoData()] as const,
   transform: transformPlayerToLoaderData,
});

Some pros of this approach include…

  • Less mental overhead and optional chaining in code
  • Better type certainty
  • Easy to write re-usable loaders that can be abstracted away from the components

If you want, you could move the loader into a completely different file, and re-use the same loader for a different components. Inside the component body, you can safely assume that you have initial data from the query served on the first render.

Lifecycle states

You could specify the overarching lifecycle states of the component directly in the loader:

const pokemonLoader = createLoader({
   queries: (name) => [useGetPokemon(name)] as const,
   queriesArg: (props) => props.pokemonName,
   onLoading: (props) => <div>Loading {props.pokemonName}...</div>,
   onFetching: (props) => <div>Loading {props.pokemonName}...</div>,
   onError: (props, error) => <RTKError error={error} />,
})

Or you could take more control over the lifecycle of the component, by utilizing the useLoader hook that the loader exposes:

const { useLoader as usePokemonLoader } = createLoader(...);

const Pokemon = () => {
   const query = usePokemonLoader(props.pokemonName);
   if (query.isLoading){ ... }
   if (query.isFetching){ ... }
   if (query.isError){ ... }
   if (query.isSuccess){ ... }
} 

Extending a loader

Extending a loader is as simple as using .extend and passing the arguments that you want to overwrite:

const baseLoader = createLoader({
   onLoading: () => <div>Loading...</div>
});

const pokemonLoader = baseLoader.extend({
   queries: () => [...],
})

Does this replace queries directly inside components?

No, definitely not. Loaders are meant for seeding the component with initial data. You could refetch the loader-queries from the component, or based on props, but there’s nothing wrong using queries directly inside of the component itself as well, if you don’t need them to load before the component renders.

Where can I get this 🤩?

You can install it right now, from npm:

npm i @ryfylke-react/rtk-query-loader