RTK Query Loader

website·Indexed 31 days ago

https://rtk-query-loader.ryfylke.dev/

A library for creating extendable, transformable, and reusable loaders for React components. It manages loading, fetch, and error state views for components depending on external data, featuring tools like createLoader(), withLoader(), and <AwaitLoader />. It supports aggregating multiple queries into a single state, deferring long-running queries, and integration with other query-fetching libraries such as Tanstack Query.

Tokens
7.8K
Snippets
37
Records
42
Agent score
57%

What's inside RTK Query Loader

  1. Reduce boilerplate for RTK Query loading and error states

    RTK Query Loader is designed to remove the tedious logic of checking isLoading and isError states for multiple queries within a component. It allows developers to isolate data-loading logic from presentational components, increasing type certainty by reducing the need for optional chaining and allowing components to be written as if the data is already present. This provides a 'loader' pattern similar to those found in server-side rendering frameworks (like Remix or Next.js) but for Single Page Applications (SPAs).
    // Traditional approach without RTK Query Loader:
    const Component = () => {
      const pokemonQuery = useGetPokemon("charizard");
      const userQuery = useGetCurrentUser();
      const userStatsQuery = useGetUserStats();
    
      if (
        pokemonQuery.isLoading ||
        userQuery.isLoading ||
        userStatsQuery.isLoading
      ) {
        return <LoadingView />;
      }
    
      if (
        pokemonQuery.isError ||
        userQuery.isError ||
        userStatsQuery.isError
      ) {
        return (
          <ErrorView
            error={
              pokemonQuery.error ??
              userQuery.error ??
              userStatsQuery.error
            }
          />
        );
      }
    
      return (
        <ComponentWithData
          pokemon={pokemonQuery.data}
          user={userQuery.data}
          userStats={userStatsQuery.data}
        />
      );
    };
  2. Use RTK Query Loader with other data-fetching libraries

    RTK Query Loader is designed with @reduxjs/toolkit in mind, but it can be adapted to any similar data loading solution through the use of resolvers. A resolver is a function that transforms the output of a third-party query hook into a compatible state object that RTK Query Loader can process.
  3. Install RTK Query Loader

    Install the @ryfylke-react/rtk-query-loader package via npm to enable the creation of loaders for React components.
    npm i @ryfylke-react/rtk-query-loader
  4. Extend the base loader to inherit default configurations

    To maintain sensible defaults while adding custom logic, extend the baseLoader using the .extend() method. This allows you to overwrite specific configurations as needed. The .extend() method accepts the same arguments as the createLoader function.
    import { baseLoader } from "./baseLoader";
    
    export const userRouteLoader = baseLoader.extend({
      // Pass custom loader configurations here
    });
  5. Choose a method to consume RTK Query loaders

    Depending on how you want to handle loading and error states, choose one of the following methods to consume a loader:

    • withLoader: Use this when the loader should affect the entire component. It ensures the component is only rendered when it has data and is the recommended optimal approach.
    • AwaitLoader: Use this for conditional loading or when only a small section of a component should trigger data loading after the rest of the component has rendered.
    • useLoader: A hook used internally by the other two methods. Use this if you only need the aggregated queries but do not need to track the loader's overall loading or error states.
  6. Migrate loader query definitions to v1.0.0

    v0.x
    In version 1.0.0, the separate queries and deferredQueries arguments in createLoader have been merged into a single useQueries function. Instead of returning arrays of results, useQueries now returns an object containing queries and deferredQueries keys. This change eliminates the previous requirement to use as const for correct typing and removes the necessity of using transform specifically to expose deferred queries to consumers.
    // Previously (v0.x)
    const loader = createLoader({
      queries: (arg: string) => {
        const pokemonQuery = useGetPokemon(arg);
        return [pokemonQuery] as const;
      },
      deferredQueries: () => {
        const otherQuery = useOtherQuery();
        return [otherQuery] as const;
      },
    });
    
    // In version 1.0.0:
    const loader = createLoader({
      useQueries: (arg: string) => {
        const pokemonQuery = useGetPokemon(arg);
        const otherQuery = useOtherQuery();
    
        return {
          queries: { pokemonQuery },
          deferredQueries: { otherQuery },
        };
      },
    });
  7. Create a base loader for global loading and error states

    Use createLoader from @ryfylke-react/rtk-query-loader to define default fallback views for loading and error states. This base loader can then be used by consumer components to ensure consistent UI behavior when data is fetching or an error occurs.
    import {
      createLoader,
      withLoader,
    } from "@ryfylke-react/rtk-query-loader";
    import { Loading, GenericErrorView } from "../components/common";
    
    export const baseLoader = createLoader({
      onLoading: () => <Loading />,
      onError: (props, error) => <GenericErrorView error={error} />,
    });
  8. Extend and inherit properties from an existing Loader

    The .extend() method allows you to create a new Loader by inheriting properties from an existing one. You can overwrite specific properties (such as onLoading, onError, or transform) while keeping others from the parent. Extensions can be chained to create a hierarchy of loaders, sharing logic across multiple components.
    const parentLoader = createLoader({
        onLoading: () => <div>Loading...</div>
    });
    
    const childLoader = parentLoader.extend({
        useQueries: () => ({...}),
        onError: () => <div>Error</div>,
    }).extend({
        transform: () => ({...}),
    }).extend({
        onLoading: () => <div>Overwritten loading...</div>,
    });
  9. Defer slow queries to prevent blocking component load

    Use the deferredQueries object within the useQueries function of createLoader to specify queries that should not postpone the initial load of a component. While standard queries in the queries object block the loading state until resolved, deferredQueries allow the component to render while the slow query continues to resolve in the background. In the transform function, data from deferred queries will be undefined until the query completes.
    const loader = createLoader({
      useQueries: () => {
        const importantQuery = useImportantQuery();
        const slowQuery = useSlowQuery();
    
        return {
          queries: {
            importantQuery,
          },
          deferredQueries: {
            slowQuery,
          },
        };
      },
      transform: (loader) => ({
        important: loader.queries.importantQuery.data, // ImportantQueryResponse
        slow: loader.deferredQueries.slowQuery.data, // SlowQueryResponse | undefined
      }),
    });
  10. Wrap a component with withLoader() to manage data fetching states

    withLoader() is a higher-order component that wraps a target component and provides it with data from a specified Loader (created via createLoader). It automatically handles the rendering of load, fetch, and error states, only rendering the wrapped component once the data has been successfully loaded.

    It accepts two arguments:

    1. A component function that receives the standard props as the first argument and the loaded data as the second argument.
    2. A Loader instance.
    const pokemonLoader = createLoader({
      useQueries: () => ({
        queries: { pokemon: useGetPokemon() },
      }),
    });
    
    // Inline component definition
    const Pokemon = withLoader((props, { queries }) => {
      return <div>{queries.pokemon.data.name}</div>;
    }, pokemonLoader);
  11. Expose loader state handlers to components via payload

    To allow a component to update the state inside a Loader, include the state setters (or other handlers) in the payload object returned by useQueries. These handlers are then accessible to the component wrapped by withLoader through the loader.payload property.
    const componentLoader = createLoader({
      useQueries: () => {
        const [name, setName] = useState("charizard");
        const debouncedName = useDebounce(name, 200);
        const pokemon = useGetPokemon(debouncedName);
        return {
          queries: {
            pokemon,
          },
          payload: { name, setName },
        };
      },
    });
    
    const Component = withLoader((props, loader) => {
      const onChange = (e) => loader.payload.setName(e.target.value);
      return <input onChange={onChange} />;
    }, componentLoader);
  12. Extend loaders using only the transform function

    v0.x
    In version 1.0.0, the restriction requiring transform and queries to be extended together has been removed. You can now extend a loader by providing only a transform function without needing to supply queries.