Lets you create loaders that contain multiple RTK queries.
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 {
createLoader,
withLoader,
} 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);
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
Creates a Loader
.
const loader = createLoader({
queries: () => [useGetUsers()] as const,
});
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, Component: (() => ReactElement)) => ReactElement
Make sure you call the second argument as a component, not a function:
{
onFetching: (props, Component) => (
<div className="relative-wrapper">
<Component />
<LoadingOverlay />
</div>
);
}
whileFetching?:
{
append?: (props: P, data?: R) => ReactElement;
prepend?: (props: P, data?: R) => ReactElement;
}
By using this instead of onFetching
, you ensure that you don't reset the internal state of the component while fetching.
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
)
(props: P, loaderData: R) => ReactElement
Component with loader-dataLoader
Return value ofcreateLoader
.
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 />,
}),
)
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>;
}
//...
};
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.
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.
NOTE:
If the loader that you extend from has a
transform
function, and you are change thequeries
function in the extended loader, you might need to do the following fix to resolve the types correctly:
const baseLoader = createLoader({
queries: () => [...],
transform: () => {i_want: "this-format"},
})
// This first example is extending a loader that has a transform.
// It does not supply a new transform function
const extendedOne = baseLoader.extend(({
queries: () => [...],
}))
type TestOne = InferLoaderData<typeof extendedOne>;
// Resolves to: { i_want: string; }
// which is incorrect. In reality it defaults to your list of queries.
// In this example, we supply a transform function as well:
const extendedTwo = baseLoader.extend({
queries: () => [...],
transform: (q) => q, // This is essentially the default value
});
type TestTwo = InferLoaderData<typeof extendedTwo>;
// Resolves to: readonly [UseQueryResult<...>]
// which is correct.
This is just a type mistake that will hopefully be fixed in the future. Both
extendedOne
andextendedTwo
return the same format, butextendedTwo
has the correct types.