loading...
Cover image for How to urql, basics

How to urql, basics

jovidecroock profile image Jovi De Croock Originally published at formidable.com ・5 min read

urql, the graphql-client that grows with you (2 Part Series)

1) How to urql, basics 2) How to urql, authentication & multiple users

When picking a GraphQL client for React, many default to using Apollo or Relay, but now there’s a new kid on the block rising in popularity over the last year: Its name is urql. It's not as packed with features as other GraphQL clients. Instead, urql aims to be minimal and highly customizable. This blog post series will start by walking you through getting started with urql, and then move on to more advanced topics like subscriptions, normalised caching, etc.

Concepts

This blog series assumes a basic understanding of GraphQL. The following basic urql concepts will also be referenced throughout the series.

Operations

In urql all operations are controlled by a central client. This client is responsible for managing GraphQL operations and sending requests. This includes things like queries, mutations, and subscriptions.

A typical Request Operation looks like this:

{
  key: 'hash',
  operationName: 'query | mutation | subscription | teardown',
  variables: {},
  context: {
        fetchOptions: 'function | object',
        requestPolicy: 'cache-first | cache-only | network-only | cache-and-network',
        url: 'string'
  }
}

The most important properties are listed in the above example; more properties can be found here.

The above key property is a hash of the querystring + variables used for this operation. This key uniquely identifies every operation, so, if we have two components dispatching the same query with the same variables, we can purposely ignore one of them to avoid duplicate requests.

With the requestPolicy we can dictate whether or not we want to use our cache, and whether or not we want to fetch even if there is a cache-hit.fetchOptions allows us to dictate what headers and other options to use with the fetch action.

When an Operation comes back as a cache-hit or as a fetched result we start calling it an OperationResult. This will typically look like this:

{
  operation, // the operationRequest mentioned earlier
  errors, // our possible server response errors
  data, // the data received
  extensions // possible extensions attached to the response by your backend
} 

An OperationResult will then be handled by exchanges before reaching the client.

Exchanges

Exchanges are middleware-like extensions that handle how operations flow through the client and how they're fulfilled. Multiple exchanges may handle each operation.

You can pass in these exchanges to the client like this:

createClient({ exchanges: [exchange1, exchange2, ...] });

The exchanges will be executed in the order provided to the client. This means that when an operation comes in, exchange1 will be called. When exchange1 is done, the operation gets forwarded to exchange2 and so on. When the last exchange completes, we get an OperationResult. This OperationResult is then sent back through the chain of exchanges in the reverse direction, and finally reaches the client.

More info around exchanges can be found here.

__Typename

Every type we make in our graphql-server will have a name and send it back when we query the __typename field. For example, the entity below will implicitly have an additional __typename: 'Todo' field.

type Todo {
  id: ID!
  text: String
  completed: Boolean
}

The __typename field is useful for identifying the queries affected by a certain mutation. When a mutation receives a response with a __typename we're currently watching with a query, then we can assume this watched query should be invalidated.

Getting started

If you want to follow along you can use this template.

For this walkthrough we'll be using React.js but note that urql can be used outside of React.

Starting out with urql is pretty convenient. First, we create our client. This client will process the operations and their results.

// App.js
import { createClient } from 'urql';

const client = createClient({
  // This url can be used in your sandbox as well.
  url: 'https://0ufyz.sse.codesandbox.io',
});

The client has more options, but the url is the only mandatory one. A few exchanges are included by default:

Find more client-options here.

Next, set up a Provider to allow our React-tree to access the client.

import { createClient, Provider } from 'urql';

const client = createClient(...);

export const App = () => (
  <Provider value={client}><Todos /></Provider>
);

At this point, our client is set up to handle incoming results, and our App has access to this client and can dispatch operations. The only thing we are still missing is actually dispatching operations, so let's make our first query:

import { useQuery } from 'urql';

const TodosQuery = `
  query {
    todos {
      id
      text
      complete
    }
  }
`;

export const Todos = () => {
  const [result] = useQuery({ query: TodosQuery });

  if (result.fetching) return <p>Loading...</p>;
  if (result.error) return <p>Oh no... {result.error.message}</p>;

  return (
    <ul>
      {result.data.todos.map(({ id, text, complete }) => (
        <Todo
          key={id}
          text={text}
          id={id}
          complete={complete}
          disabled={result.fetching}
        />)
      )}
    </ul>
  );
}

In the example above, if todo results are present in the cache they will be returned synchronously (no result.fetching) and if they're not they will be fetched.

More options for the useQuery hook can be found here.

You might worry that this architecture would result in unnecessary fetching, but the first default exchange included in your urql-client is the dedupExchange. Do you recall us talking about a unique key on each operation? We use that key to determine in that dedupExchange whether or not we already have an operation in progress for a given piece of data. When queries and variables are identical, a new fetch is not performed.

We are still missing one crucial part: we want to be able to mark a todo as completed. Let's refactor our application to allow each Todo item to toggle and persist its completed state.

import { useMutation } from 'urql';

const ToggleTodoMutation = `
  mutation($id: ID!) {
    toggleTodo(id: $id) {
      id
    }
  }
`;

export const Todo = ({ id, text, complete, disabled }) => {
  const [result, toggleTodo] = useMutation(ToggleTodoMutation);

  if (result.error) return <p>Something went wrong while toggling</p>;

  return (
    <li>
      <p onClick={() => toggleTodo({ id })}>
        {text}
      </p>
      <p>{complete ? 'Completed' : 'Todo'}</p>
      <button
        onClick={() => toggleTodo({ id })}
        disabled={disabled || result.fetching}
        type="button"
      > {complete ? 'Toggle todo' : 'Complete todo'}</button>
    </li>
  );
}

Notice the disabled={result.fetching} on our Todo component. Our example uses a document-based cache, so when we do a mutation on a certain __typename, queries associated with this type will be refetched. In our case, toggling the completed state of our Todo type will cause our todos query will be refetched, so we prevent additional toggles while the result is fetching.

Try opening the network-tab of your browser when this mutation completes. You'll see a query being triggered to refetch our todos. This is because our cacheExchange sees a mutation response with the typename "Todo"; it knows that we are currently watching an array of this type and invalidates it, triggering the refetch.

If you'd like to dig into exactly how caching and the dedupExchange is working, you can delay the mounting of this second component until the first has fetched. You will see the data for the query return synchronously, thanks to our cacheExchange. The default cache will save responses by their operation key.

You can also try altering the default caching behavior by changing the requestPolicy from the default cache-first to cache-and-network. This will force the query to refetch in the background.

More options for the useMutation hook can be found here.

Conclusion

This was an introduction to urql, the new kid on the block for GraphQL clients. In the future we'll cover how to setup subscriptions, server-side rendering, and more.

We hope you learned something and are as excited as we are about this new library!

This is a cross-post from Formidable

urql, the graphql-client that grows with you (2 Part Series)

1) How to urql, basics 2) How to urql, authentication & multiple users

Posted on by:

jovidecroock profile

Jovi De Croock

@jovidecroock

Belgium - 24 years old - Preact & urql core-team - maker of tiny libraries - JavaScript/TypeScript - React.JS - GraphQL - OSS enthusiast

Discussion

markdown guide