Options
All
  • Public
  • Public/Protected
  • All
Menu

gqless-hooks

gqless-hooks

npm version bundlephobia license combined checks codecov

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

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',
  }
);

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

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.

Generated using TypeDoc