Skip to content

Releases: ryfylke-react-as/rtk-query-loader

@ryfylke-react/rtk-query-loader@0.3.32

30 Oct 11:09
Compare
Choose a tag to compare
  • onError is now passed a third argument, query, which contains the joined query for the loader
  • Fixed/moved dependencies to reduce bundle-size
  • Did some refactoring to withLoader so that we now don't call the passed component as a function, but instead by using React.createElement/JSX (and a forwardRef to pass the loader data)
  • New argument: whileFetching, ensures that the component is not unmounted while fetching - which might happen when using onFetching.
  • Updated docs
  • Enhanced test coverage

@ryfylke-react/rtk-query-loader@0.3.22

25 Oct 18:45
Compare
Choose a tag to compare
  • Fixed error with InferLoaderData where previously it could not infer the loader data if one of the loader's generics was never.

Initial release

24 Oct 11:02
c64971a
Compare
Choose a tag to compare

@ryfylke-react/rtk-query-loader@0.3.21

npm
npm type definitions
npm bundle size

Lets you create loaders that contain multiple RTK queries.

Usage

yarn add @ryfylke-react/rtk-query-loader
# or
npm i @ryfylke-react/rtk-query-loader

A simple example of a component using rtk-query-loader:

import {
  createUseLoader,
  RTKLoader,
} from "@ryfylke-react/rtk-query-loader";

const loader = createLoader({
  queries: () => {
    const pokemon = useGetPokemon();
    const currentUser = useGetCurrentUser();
    return [pokemon, currentUser] as const;
  },
  onLoading: () => <div>Loading pokemon...</div>,
});

const Pokemon = withLoader((props, queries) => {
  const pokemon = queries[0].data;
  const currentUser = queries[1].data;

  return (
    <div>
      <h2>{pokemon.name}</h2>
      <img src={pokemon.image} />
      <a href={`/users/${currentUser.id}/pokemon`}>
        Your pokemon
      </a>
    </div>
  );
}, loader);

What problem does this solve?

Let's say you have a component that depends on data from more than one query.

function Component(props){
  const userQuery = useGetUser(props.id);
  const postsQuery = userGetPostsByUser(userQuery.data?.id, {
    skip: user?.data?.id === undefined,
  });

  if (userQuery.isError || postsQuery.isError){
    // handle error
  }

  /* possible something like */
  // if (userQuery.isLoading){ return (...) }

  return (
    <div>
      {/* or checking if the type is undefined in the jsx */}
      {(userQuery.isLoading || postsQuery.isLoading) && (...)}
      {userQuery.data && postsQuery.data && (...)}
    </div>
  )
}

The end result is possibly lots of bloated code that has to take into consideration that the values could be undefined, optional chaining, etc...

What if we could instead "join" these queries into one, and then just return early if we are in the initial loading stage. That's basically the approach that rtk-query-loader takes. Some pros include:

  • Way less optional chaining in your components
  • Better type certainty
  • Easy to write re-usable loaders that can be abstracted away from the components

Exports

createLoader

Creates a Loader.

const loader = createLoader({
  queries: () => [useGetUsers()] as const,
});

Argument object:

queries?: (arg?: T) => readonly UseQueryResults<unknown>[]

Returns a readonly array of useQuery results.

transform?: (queries: readonly UseQueryResult[]) => T

Transforms the list of queries to the desired loader output format.

queriesArg?: (props: T) => A

Creates an argument for the queries function based on expected props. Useful when you have queries in your loader that need arguments from the props of the component.

onLoading?: (props: T) => ReactElement

onError?: (props: T, error: RTKError) => ReactElement

onFetching?: (props: T, renderBody: (() => ReactElement)) => ReactElement

withLoader

Wraps a component to provide it with loader data.

const postsLoader = createLoader(...);

const Component = withLoader(
  (props: Props, loaderData) => {
    // Can safely assume that loaderData and props are populated.
     const posts = loaderData.posts;

     return posts.map(,,,);
  },
  postsLoader
)

Arguments

  1. (props: P, loaderData: R) => ReactElement
    Component with loader-data
  2. Loader
    Return value of createLoader.

Extending/customizing the loader

To use an existing loader but with maybe a different loading state, for example:

const Component = withLoader(
  (props: Props, loaderData) => {
    // Can safely assume that loaderData and props are populated.
     const posts = loaderData.posts;

     return posts.map(,,,);
  },
  postsLoader.extend({
    onLoading: (props) => <props.loader />,
    onFetching: (props) => <props.loader />,
  }),
)

createUseLoader

Creates only the hook for the loader, without the extra metadata like loading state.

Basically just joins multiple queries into one, and optionally transforms the output. Returns a standard RTK useQuery hook.

A good solution for when you want more control over what happens during the lifecycle of the query.

const useLoader = createUseLoader({
  queries: (arg: string) =>
    [
      useQuery(arg.query),
      useOtherQuery(arg.otherQuery),
    ] as const,
  transform: (queries) => ({
    query: queries[0].data,
    otherQuery: queries[1].data,
  }),
});

const Component = () => {
  const query = useLoader();

  if (query.isLoading) {
    return <div>loading...</div>;
  }
  //...
};

InferLoaderData

Infers the type of the data the loader returns. Use:

const loader = createLoader(...);
type LoaderData = InferLoaderData<typeof loader>;

Typescript should infer the loader data type automatically inside withLoader, but if you need the type elsewhere then this could be useful.

Extending loaders

You can extend a loader like such:

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

const pokemonLoader = baseLoader.extend({
  queries: (name: string) => [useGetPokemon(name)],
  queriesArg: (props: PokemonProps) => props.name.toLowerCase(),
});

New properties will overwrite existing.

If the loader you extend has a transform function, and you are changing the queries function, you might need to do this to resolve the types properly:

const baseLoader = createLoader({
  queries: () => [...],
  transform: () => {i_want: "this-format"},
})

const pokemonLoader = baseLoader.extend({
  queries: () => [...],
  transform: (q) => q, // Reapply default transform for query
});

type Test = ReturnType<typeof pokemonLoader.useLoader>;
// { i_want: string; }

Full Changelog: https://github.com/ryfylke-react-as/rtk-query-loader/commits/v0.3.21