FormidableLabs/urql

Name: urql

Owner: Formidable

Description: Universal React Query Library

Created: 2018-01-24 01:44:42.0

Updated: 2018-02-19 20:17:07.0

Pushed: 2018-02-18 22:19:45.0

Homepage: null

Size: 367

Language: TypeScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

urql

Universal React Query Library

Build Status Build status Coverage Status npm npm

Urkel

What is urql

urql is a GraphQL client, exposed as a set of ReactJS components.

Why this exists

In my experience, existing solutions have been a bit heavy on the API side of things, and I see people getting discouraged or turned away from the magic that is GraphQL. This library aims to make GraphQL on the client side as simple as possible.

How it's different
React

urql is specifically for React. There have been no efforts made to abstract the core in order to work with other libraries. Usage with React was a priority from the get go, and it has been architected as such.

Render Props

urql exposes its API via render props. Recent discussion has shown render props to be an extraordinarily flexible and appropriate API decision for libraries targeting React.

Caching

urql takes a unique approach to caching. Many existing solutions normalize your data and parse your queries to try to invalidate cached data. I am not smart enough to implement this solution, and further, normalizing everything, on big datasets, can potentially lead to performance/memory issues.

urql takes a different approach. It takes your query signature and creates a hash, which it uses to cache the results of your query. It also adds __typename fields to both queries and mutations, and by default, will invalidate a cached query if it contains a type changed by a mutation. Further, handing control back to the users, it exposes a shouldInvalidate prop, which is a function that can be used to determine whether the cache is invalid based upon typenames, mutation response and your current data.

Install

npm install urql --save

Getting Started

If you want to get right down to business and try a working example of urql in action, check out this Code Sandbox:

https://codesandbox.io/s/p5n69p23x0

The core of urql is three exports, Provider, Connect and Client. To get started, you simply create a Client instance, pass it to a Provider and then wrap any components you want to make queries or fire mutation from with a Connect component. We also provide a ConnectHOC higher order component, if you happen to not enjoy the absolutely amazing explicit nature of render props.

Lets look at a root level component and how you can get it set up:

rt React from 'react';
rt ReactDOM from 'react-dom';

rt { Provider, Client } from 'urql';
rt Home from './home';

t client = new Client({
l: 'http://localhost:3001/graphql',


rt const App = () => (
rovider client={client}>
<Home />
Provider>


tDOM.render(<App />, document.getElementById('root'));

As you can see above, all that's required to get started is the url field on Client which tells us where your GraphQL API lives. After the client is created, and passed to the Provider that wraps your app, now you can wrap any component down in the tree with a Connect to start issuing queries.

Queries and mutations both have creation functions, which you can import. An urql Connect component can take multiple queries, and multiple mutations. The render prop exposes the internal logic to any component you'd like to provide it to.

Lets start by defining a query and a mutation:

t TodoQuery = `
y {
dos {
id
text



HOLD UP FAM THIS IS IMPORTANT

It is absolutely necessary if you want this library to work properly, to create a valid mutation response. If you change a todo, return it. If you delete a todo, return it. If you add a todo, return it. If you don't return the thing that changed and file an issue, I'm going to screenshot this paragraph, paste it into the issue, and then drop my finger from a 3ft height onto the close button while making plane crash sounds.

t AddTodo = `
tion($text: String!) {
dTodo(text: $text) {
id
text



Now we can use the mutation and query functions to format them in the way urql expects.

t Home = () => (
onnect
query={query(TodoQuery)}
mutation={{
  addTodo: mutation(AddTodo),
}}
children={({ loaded, fetching, refetch, data, error, addTodo }) => {
  //...Your Component
}}


You can also use functional child style:

t Home = () => (
onnect
query={query(TodoQuery)}
mutation={{
  addTodo: mutation(AddTodo),
}}

{({ loaded, fetching, refetch, data, error, addTodo }) => {
  //...Your Component
}}
Connect>

The children render prop sends a couple of fields back by default:

Also, any mutations, because they are named, are also passed into this render prop.

As you can see above, the query accepts either a single query, or an array of queries. The mutation prop accepts an object, with the mutation names as keys.

So why do we use these query and mutation functions before passing them? Variables, thats why. If you wanted to pass a query with variables, you would construct it like so:

rt { query } from 'urql';

y(TodoQuery, { myVariable: 5 });

Similarly, you can pass variables to your mutation. Mutation, however is a bit different, in the sense that it returns a function that you can call with a variable set:

rt { mutation } from 'urql';

tion(AddTodo); // No initial variables

fter you pass 'addTodo' from the render prop to a component:

odo({ text: `I'm a variable!` });
Cache control

Normally in urql, the cache is aggressively invalidated based upon __typename, but if you want finer grained control over your cache, you can use the shouldInvalidate prop. It is a function, that returns a boolean, much like shouldComponentUpdate, which you can use to determine whether your data needs a refresh from the server. It gets called after every mutation:

t MyComponent = () => (
onnect
query={query(MyQuery)}
shouldInvalidate={(changedTypenames, typenames, mutationResponse, data) => {
  return data.todos.some(d => d.id === mutationResponse.id);
}}
children={({ loaded, fetching, refetch, data, error, addTodo }) => {
  //...Your Component
}}


The signature of shouldInvalidate is basically:

Using all or some of these arguments can give you the power to pretty accurately describe whether your connection has now been invalidated.

Custom Caches

The Client constructor accepts a cache setting where you can provide your own caching mechanism that will work with urql. By default, we use a local object store, but you can provide an adapter for whatever you want.

If you want to supply your own cache, you'll want to provide an object with the following keys:

Don't worry about the hashes, we convert query objects(query + variables) to the hash behind the scenes. Here is an example of the cache creation function we use internally for reference:

t defaultCache = store => {
turn {
invalidate: hash =>
  new Promise(resolve => {
    delete store[hash];
    resolve();
  }),
invalidateAll: () =>
  new Promise(resolve => {
    store = {};
    resolve();
  }),
read: hash =>
  new Promise(resolve => {
    resolve(store[hash] || null);
  }),
update: callback =>
  new Promise(resolve => {
    if (typeof callback === 'function') {
      Object.keys(store).map(key => {
        callback(store, key, store[key]);
      });
    }
    resolve();
  }),
write: (hash, data) =>
  new Promise(resolve => {
    store[hash] = data;
    resolve();
  }),


API
Client

{url: string, initialCache?: object, cache?: Cache, fetchOptions?: object | () => object}

Client is the constructor for your GraphQL client. It takes a configuration object as an argument, which is required. Providing a GraphQL api url via the url property is required. fetchOptions are the options provided to internal fetch calls, which can either be in object format, or a function that returns an object, in case you want to provide a dynamic header for a token or something.

initialCache is an initial state for your cache if you are using the default cache. This probably won't get much play until SSR is implemented.

cache accepts an instance of a cache, if you want to build your own custom one built with something like AsyncStorage. You can read more about how to create one of these above.

Example:

t client = new Client({ url: 'http://localhost:3000/graphql' });
Provider

Provider is a ReactJS component that is used to provide the urql client throughout your application.

Example:

t client = new Client({ url: 'http://localhost:3000/graphql' });
.
rn (
rovider client={client}>
<YourApp />
Provider>

Connect

Connect is a ReactJS component that is used to execute queries and mutations and render child components with the results, using a render prop.

Props

| Name | Value | Default | Description | | —————- | —————————————————————————- | ———- | —————————————————————————— | | query | QueryObject or [QueryObject] | null | The query/queries you want connected to your component | | mutation | MutationMap | null | The mutation/mutations you want connected to your component | | cache | boolean | true | Whether this component's queries should be cached | | typeInvalidation | boolean | true | Whether this component's cache should be invalidated using typeNames | | shouldInvalidate | (changedTypes, componentTypes, mutationResponse, componentData) => boolean | null | Function used to determine whether the component's cache should be invalidated | | children | ({RenderArgs}) | RenderArgs | Render prop used to render children |

Render Args

The following fields are present on the render functions argument object:

| Name | Value | Default | Description | | ——————- | ———- | ———- | —————————————————————————————————— | | cache | object | Cache | Provides cache operations, defined below | | fetching | boolean | false | Fetching is true during any pending query or mutation operation | | loaded | boolean | false | Becomes true once the component gets data for the first time. | | error | object | null | Any errors thrown during a query or mutation | | data | object | null | Any data returned as the result of a query | | refetch | function | function | Function used to refetch existing queries, can skip cache by calling with {skipCache: true} argument | | refreshAllFromCache | function | function | Function used to refetch all queries from the cache. |

The cache object provides several helpful cache methods that you can use to control the cache:

In addition to these, any specified mutations are also provided as their key in the mutation map. Mutations are functions that accept an object of variables as an argument.

Example:

nect
ery={query(MyQuery)}
ildren={({loaded, data}) => {
return loaded ? <Loading/> : <List data={data.todos}>



ith mutations

nect
tation={{
addTodo: mutation(AddTodo)

ildren={({ addTodo }) => {
return <button type="button" onClick={addTodo}>Add Todo</button>


ConnectHOC

(options: object | (props) => object) => (Component)

ConnectHOC is a higher order component that essentially does the same thing as the Connect component. All of Connect's props except for render are valid for the options object. Further, you can specify a function, which will provide the component's props and return a dynamic option set. The arguments you'd see in the render prop in Connect are passed automatically to the wrapped component.

Example:

rt default ConnectHOC({
ery: query(TodoQuery)
yComponent);

r

rt default ConnectHOC((props) => {
ery: query(TodoQuery, { id: props.id })
yComponent);
query

(query: string, variables?: object) => {query: string, variables: object}

query is a QueryObject creator.

Example:

y(

y($id: ID!) {
dos(id: $id) {
text


id: 5 }

mutation

(query: string, variables?: object) => {query: string, variables: object}

query is a MutationObject creator.

Example:

tion(

tion($id: ID!) {
dTodo(id: $id) {
text


id: 5 }

TODO
Prior Art
Apollo

This library wouldn't be possible without Apollo. Apollo was what made GraphQL click for me. I need to give big shout outs to folks like @stubailo, @jbaxleyiii and @peggyrayzis, without whom I wouldn't even know GraphQL. Enormous amounts of inspiration for this lib came from Apollo and its architecture.


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.