gqless-hooks
v2.0.1
Published
[![npm version](https://badge.fury.io/js/gqless-hooks.svg)](https://badge.fury.io/js/gqless-hooks) [![bundlephobia](https://badgen.net/bundlephobia/minzip/gqless-hooks)](https://bundlephobia.com/result?p=gqless-hooks) [![license](https://badgen.net/github
Downloads
10
Maintainers
Readme
gqless-hooks
yarn add gqless-hooks
# or
npm install gqless-hooks
This library creates a couple of hooks to interact with gqless, all while being type-safe.
If you are not familiar with gqless please check https://gqless.dev/
Table of Contents
Usage
This library should ideally be imported and used at src/graphql/client.ts (this is default location, could be anywhere you previously set it up)
import { Client, QueryFetcher } from 'gqless';
import { Mutation, Query, schema } from './generated';
import { createUseMutation, createUseQuery } from 'gqless-hooks';
const endpoint = '...';
// ...
export const query = client.query;
export const useMutation = createUseMutation<Mutation>({
endpoint,
schema,
});
export const { useQuery, prepareQuery } = createUseQuery<Query>({
endpoint,
schema,
});
Then anywhere you want to use them, you import them just like the default vanilla query.
import { useMutation, useQuery } from '../src/graphql';
const Component = () => {
const [helloWorldMutation, helloWorldData] = useMutation(
({ helloWorldMutation }) => {
const { id, label } = helloWorldMutation({ arg: 'hello' });
return { id, label };
}
);
// helloWorldData === { data, state = "loading" | "error" | "waiting" | "done", errors = GraphqlError[] | undefined }
// helloWorldMutation works as initiator of the mutation or recall.
const [helloWorldData, { callback, refetch, cacheRefetch }] = useQuery(
({ helloWorldQuery: { id, label } }) => ({ id, label }),
{
// if lazy == true, wait until function from returned array is called
lazy: true,
}
);
// helloWorldData === { data = { id,label } | undefined | null, state = "loading" | "error" | "waiting" | "done", errors = GraphqlError[] | undefined }
// callback and refetch work as initiators of the query or refetch.
};
Features
- Cache policies, somewhat following Apollo fetchPolicy
- Shared global cache.
- Polling
- Automatic refetch on variables change
- Support for Pagination with a fetchMore callback.
- Server side rendering support (with usage examples for Next.js)
- Prefetching support
Docs and API Reference
You can check https://pabloszx.github.io/gqless-hooks/ for some documentation and API Reference, all generated through it's strong type-safety using TypeDoc.
Also keep in mind that these hooks are heavily inspired by React Apollo GraphQL
- useQuery is inspired by Apollo useQuery
- useMutation is inspired by Apollo useMutation
Usage tips
Due to how gqless works, in the query and mutation hook functions, when you return some data, you have to explicitly access it's properties for it to detect it's requirements, this means in practice that if you have an object, you have to explictly explore its properties (destructuring for example) and return them, and for arrays is the same, but for them it's recommended to use array.map(...).
For example
useQuery(
(schema, variables) => {
// variables === { b: 2 }
const { field1, field2 } = schema.helloWorldObj;
return { field1, field2 };
// return helloWorldObj; <- would return an empty object
},
{
variables: {
b: 2,
},
}
);
useQuery(
(schema, variables) => {
// variables === { a: 1 }
const array = schema.helloWorldArray;
return array.map(({ fieldA, fieldB }) => ({ fieldA, fieldB }));
// return array; <- would return an empty array
},
{
variables: {
a: 1,
},
}
);
Headers
You can set headers to be added to every fetch call
export const useQuery = createUseQuery<Query>({
schema,
endpoint,
creationHeaders: {
authorization: '...',
},
});
or individually
//useMutation((schema) => {
useQuery(
(schema) => {
//...
},
{
//...
headers: {
authorization: '...',
},
}
);
Polling
You can set a polling interval in milliseconds
useQuery(
(schema) => {
//...
},
{
//...
pollInterval: 100,
}
);
Shared cache and in memory persistence
You can specify that some hooks actually refer to the same data, and for that you can specify a sharedCacheId that will automatically synchronize the hooks data, or persist in memory hooks data.
Be careful and make sure the synchronized hooks share the same data type signature
// useMutation((schema) => {
useQuery(
(schema) => {
//...
},
{
//...
sharedCacheId: 'hook1',
}
);
// another component
// useMutation((schema) => {
useQuery(
(schema) => {
//...
},
{
// You could also specify the cache-only fetchPolicy
// To optimize the hook and prevent unwanted
// network fetches.
fetchPolicy: 'cache-only',
//...
sharedCacheId: 'hook1',
}
);
You also can manipulate the shared cache directly using setCacheData
and prevent unnecessary network calls or synchronize different hooks.
import { setCacheData } from 'gqless-hooks';
// This declaration is optional type-safety
declare global {
interface gqlessSharedCache {
hookKey1: string[];
}
}
setCacheData('hookKey1', ['hello', 'world']);
// ...
useQuery(
(schema) => {
// ...
},
{
// ...
sharedCacheId: 'hookKey1',
}
);
Pagination
For pagination you can use fetchMore from useQuery, somewhat following Apollo fetchMore API.
const [{ data }, { fetchMore }] = useQuery(
(schema, { skip, limit }) => {
const {
nodes,
pageInfo: { hasNext },
} = schema.feed({
skip,
limit,
});
return {
nodes: nodes.map(({ _id, title }) => {
return {
_id,
title,
};
}),
pageInfo: {
hasNext,
},
};
},
{
variables: {
skip: 0,
limit: 5,
},
}
);
// ...
if (data?.hasNext) {
const newData = await fetchMore({
variables: {
skip: data.length,
},
updateQuery(previousResult, newResult) {
if (!newResult) return previousResult;
// Here you are handling the raw data, not "accessors"
return {
pageInfo: newResult.pageInfo,
nodes: [...(previousResult?.nodes ?? []), ...newResult.nodes],
};
},
});
}
getAccessorFields | getArrayAccessorFields
When using this library there is a common pattern in the schema -> query functions which is just destructuring the data you need from the query, the problem is that it tends to be very repetitive, and for that this library exports a couple of utility functions that help with this problem.
These functions are designed to help with autocomplete and type-safety.
Keep in mind that these functions are composable.
import { getAccessorFields } from 'gqless-hooks';
useQuery((schema, variables) => {
// This is the long way
// const { title, content, publishedData } =
// schema.blog({ id: variables.id });
// return { title, content, publishedData };
// This is the quicker way
return getAccessorFields(
schema.blog({ id: variables.id }),
'title',
'content',
'publishedDate'
);
});
import { getArrayAccessorFields } from 'gqless-hooks';
useQuery((schema) => {
// This is the long way
// return schema.blogList.map({ title, content, publishedData }
// => ({ title, content, publishedData }));
// This is the quicker way
return getArrayAccessorFields(
schema.blogList,
'title',
'content',
'publishedData'
);
});
prepareQuery (SSR, prefetching, refetch, type-safety)
You can use prepareQuery generated from createUseQuery, in which you give it a unique cache identifier
and the schema -> query
function, and it returns an object containing:
- The
query
function. - The
cacheId
. - An async function called
prepare
. - A React Cache Hydration Hook
useHydrateCache
. - A
useQuery
shorthand hook that already includes the query and the cacheId. - A shorthand
setCacheData
function to manually update the cache and hooks data. - A TypeScript-only
dataType
helper.
Keep in mind that the example as follows uses prepare as a SSR helper, but you could also use it client side for prefetching or refetching, and/or use the checkCache boolean argument option.
This example is using Next.js getServerSideProps, but follows the same API for getStaticProps or any other implementation.
import { NextPage, GetServerSideProps } from 'next';
import { prepareQuery, useQuery } from '../src/graphql';
const HelloQuery = prepareQuery({
cacheId: 'helloWorld',
query: (schema) => {
return schema.hello({ arg: 'world' });
},
});
interface HelloWorldProps {
helloWorld: typeof HelloQuery.dataType;
}
export const getServerSideProps: GetServerSideProps<HelloWorldProps> = async () => {
const helloWorld = await HelloQuery.prepare();
return {
props: {
helloWorld,
},
};
};
const HelloPage: NextPage<HelloWorldProps> = (props) => {
// This hydrates the cache and prevents network requests.
HelloQuery.useHydrateCache(props.helloWorld);
const [{ data }] = HelloQuery.useQuery();
return <div>{JSON.stringify(data, null, 2)}</div>;
};
Fully featured examples
Blog administration panel inside a Next.js / Nexus / Mongoose / gqless-hooks project.
About it
These hooks are a proof of concept that ended up working and is a good workaround until React Suspense is officially released (with good SSR support), along with the lack of functionality out of the box of the official gqless API, and of course, Mutation is officially supported by gqless.
If you are only using these hooks and not the default query from gqless, you don't need to use the graphql HOC, and it means less bundle size.
Future
- Add more examples of usage
- Suspense support
- Add support for Subscriptions
Contributing
Everyone is more than welcome to help in this project, there is a lot of work still to do to improve this library, but I hope it's useful, as it has been while I personally use it for some of my new web development projects.