GraphQL and React Native

Kadi Kraman

@kadikraman

The urql edition

Kadi Kraman

Engineering Manager

Formidable

Main topics

1. Why use GraphQL with React Native

 

2. Challenges of using GraphQL with React Native

 

3. Differences between using urql and Apollo Client

What this talk isn't

An intro to GraphQL

Why use GraphQL in React Native?

For the same reasons you might want to use it with React on the Web

  • smaller payloads (request what you need)

  • typed api contract (schema is shipped with the api)

  • built-in caching

  • lends itself to offline support

(with a GraphQL client)

(with a GraphQL client)

Apollo Client

GraphQL Clients for React Native

urql

Relay

React Native and GraphQL

First RN + gql project

August 2020

(using Apollo Client)

(using urql)

October 2017

Latest RN + gql project

.

.

.

.

various RN projects with and without GraphQL

.  .  .  .  .  .  .

.  .  .  .  .  .  .

.  .  .  .  .  .  .  .  .  .  .

Let's talk about urql

urql is a blazing-fast GraphQL client that supports React, Preact, and Svelte (alpha)

 our place in the ecosystem was to challenge existing solutions and to be flexible enough to adopt new ideas

- Phil Plückthun, urql core maintainer

Getting started

# npm
npm i --save urql graphql
# or yarn
yarn add urql graphql
import { createClient, Provider } from 'urql';

const client = createClient({
  url: 'https://0ufyz.sse.codesandbox.io'
});

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

Install

Create the client

npm install @apollo/client graphql
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
} from '@apollo/client';

const client = new ApolloClient({
  uri: 'https://48p1r2roz4.sse.codesandbox.io',
  cache: new InMemoryCache()
});

function App() {
  return (
    <ApolloProvider client={client}>
      <div>
        <h2>My first Apollo app 🚀</h2>
      </div>
    </ApolloProvider>
  );
}

Install

Create the client

urql

Apollo

import { useQuery } from 'urql';

const Todos = () => {
  const [res] = useQuery({
    query: `
      query { todos { id text } }
    `,
  });
  
  if (res.fetching) return <p>Loading...</p>;
  
  if (res.error) return <p>Errored!</p>;
  
  return (
    <ul>
      {res.data.todos.map(todo => (
        <li key={todo.id}>{todo.text}</li>
      ))}
    </ul>
  );
};

Query Data

import { useQuery, gql } from '@apollo/client';

const EXCHANGE_RATES = gql`
  query GetExchangeRates {
    rates(currency: "USD") {
      currency
      rate
    }
  }
`;

function ExchangeRates() {
  const { loading, error, data } = useQuery(EXCHANGE_RATES);

  if (loading) return <p>Loading...</p>;
  if (error) return <p>Error :(</p>;

  return data.rates.map(({ currency, rate }) => (
    <div key={currency}>
      <p>
        {currency}: {rate}
      </p>
    </div>
  ));
}

urql

Apollo

Exchanges

Exchanges are a series of operations that transform the input (operations) stream to the output stream

Similar to Apollo link

it [urql] is so extensible that even the cache is an exchange

- Jovi De Croock, urql core maintainer

icon from flaticon.com

useQuery

data

icon from flaticon.com

useQuery

data

dedupExchange

dedupExchange - remove duplicate from pending operations

icon from flaticon.com

useQuery

data

dedupExchange

cacheExchange - the default caching logic with "Document Caching"

cacheExchange

icon from flaticon.com

useQuery

data

dedupExchange

fetchExchange - sends an operation to the API using fetch and adds results to the output stream

cacheExchange

fetchExchange

icon from flaticon.com

useQuery

data

dedupExchange

??? - your own exchange with custom enhancements

cacheExchange

fetchExchange

???

Creating custom exchanges

import { Client, dedupExchange, fetchExchange } from 'urql';

const noopExchange = ({ client, forward }) => {
  return operations$ => {
    // <-- The ExchangeIO function
    // We receive a stream of Operations from `dedupExchange` which
    // we can modify before...
    const forwardOperations$ = operations$;
    // ...calling `forward` with the modified stream. The `forward`
    // function is the next exchange's `ExchangeIO` function, in this
    // case `fetchExchange`.
    const operationResult$ = forward(operations$);
    // We get back `fetchExchange`'s stream of results, which we can
    // also change before returning, which is what `dedupExchange`
    // will receive when calling `forward`.
    return operationResult$;
  };
};

const client = new Client({
  exchanges: [dedupExchange, noopExchange, fetchExchange],
});

https://formidable.com/open-source/urql/docs/concepts/exchanges/

React Auth Exchange

const authExchange = ({ forward }) => {
  return ops$ => {
    return pipe(
      ops$,
      map(operation => {
        const token = localStorage.getItem('token');
        
        return {
          ...operation,
          context: {
            ...operation.context,
            fetchOptions: {
              ...operation.context.fetchOptions,
              headers: { Authorization: token ? `Bearer ${token}` : '' },
            },
          },
        };
      }),
      mergeMap(fromPromise),
      forward,
    );
  };
};

const client = new Client({
  exchanges: [dedupExchange, authExchange, fetchExchange],
});
import SInfo from 'react-native-sensitive-info';

const authExchange = ({ forward }) => {
  return ops$ => {
    return pipe(
      ops$,
      map(async operation => {
        const token = await SInfo.getItem('token', {});
        
        return {
          ...operation,
          context: {
            ...operation.context,
            fetchOptions: {
              ...operation.context.fetchOptions,
              headers: { Authorization: token ? `Bearer ${token}` : '' },
            },
          },
        };
      }),
      mergeMap(fromPromise),
      forward,
    );
  };
};

const client = new Client({
  exchanges: [dedupExchange, authExchange, fetchExchange],
});

React Native Auth exchange

https://github.com/FormidableLabs/urql/issues/677

import SInfo from 'react-native-sensitive-info';

const authExchange = ({ token }) => ({ forward }) => {
  return ops$ => {
    return pipe(
      ops$,
      map(operation => {
        return {
          ...operation,
          context: {
            ...operation.context,
            fetchOptions: {
              ...operation.context.fetchOptions,
              headers: { Authorization: token ? `Bearer ${token}` : '' },
            },
          },
        };
      }),
      mergeMap(fromPromise),
      forward,
    );
  };
};

const client = new Client({
  exchanges: [dedupExchange, authExchange({ token: 'token' }), fetchExchange],
});

Auth exchange - passing in variables

const authExchange = ({ logout }: { logout: () => void }): Exchange => ({
  client,
  forward,
}) => {
  // exchanges are stateful - the same instance of the token can be attached to each operation
  let state: undefined | { token: string; refreshToken: string };

  // convenience function that adds the token to each operation header and keeps track of number of attempts
  const addTokenToOperation = (operation: Operation): Operation => {
    if (!state || !state.token || operation.operationName === "teardown") {
      return operation;
    }
    const fetchOptions = operation.context.fetchOptions || {};
    return {
      ...operation,
      context: {
        ...operation.context,
        authAttempt: !operation.context.authAttempt ? 1 : 2,
        fetchOptions: {
          ...fetchOptions,
          headers: {
            ...fetchOptions.headers,
            Authorization: state.token,
          },
        },
      },
    };
  };
[---]
}

Auth exchange - fetch once

Auth Exchange

Work in progress!

Check my twitter (@kadikraman) for an update after this talk!

Caching

Document Cache

Default cache, good for simple applications

Enabling fetching the data from cache instead of querying anew each time

Request policy

import { useQuery } from 'urql';

const [res] = useQuery({
  query: `
    query { todos { id text } }
  `,
  requestPolicy: "cache-and-network",
});

cache-first (default)

cache-and-network, cache-only, and network-only

Cache invalidation

https://www.npmjs.com/package/@urql/exchange-request-policy

import { createClient, dedupExchange, cacheExchange, fetchExchange } from 'urql';
import { requestPolicyExchange } from '@urql/exchange-request-policy';
 
const client = createClient({
  url: 'http://localhost:1234/graphql',
  exchanges: [
    dedupExchange,
    requestPolicyExchange({
      // The amount of time in ms that has to go by before upgrading, default is 5 minutes.
      ttl: 60 * 1000, // 1 minute.
      // An optional function that allows you to specify whether an operation should be upgraded.
      shouldUpgrade: operation => operation.context.requestPolicy !== 'cache-only',
    }),
    cacheExchange,
    fetchExchange,
  ],
});

Graphcache

More sophisticated cache, handles interdependencies!

import { cacheExchange } from "@urql/exchange-graphcache";

const client = new Client({
  exchanges: [dedupExchange, cacheExchange({}), fetchExchange],
});

Handling cache updates after a mutation

const [res] = useQuery({
  query: `
    query { todos { id text } }
  `,
});
const UpdateTodo = `
  mutation ($id: ID!, $title: String!) {
    updateTodo (id: $id, title: $title) {
      id
      title
    }
  }
`;

const Todo = ({ id, title }) => {
  const [updateTodoResult, updateTodo] = useMutation(UpdateTodo);
};

refetchQueries?

in React Apollo

refetchQueries?

manually update cache with mutation result?

export const createPostUpdate = (cache, { data }) => {
  if (!data || !data.createPost) {
    return;
  }
  const createPost = data.createPost;
  const variables = { groupId: createPost.feedItem.data.group_id || undefined };
  const cachedFeed = cache.readQuery({
    query: GET_FEED,
    variables
  });
  const existing = cachedFeed.feed.items.path === createPost.feedItem.path;
  if (!existing) {
    cache.writeQuery({
      query: GET_FEED,
      variables,
      data: {
        ...cachedFeed,
        feed: {
          ...cachedFeed.feed,
          items: [createPost.feedItem, ...cachedFeed.feed.items]
        }
      }
    });
  }
};

refetchQueries?

manually update cache with mutation result?

let the cache handle it?

With Graphcache, the cache is automatically updated with the mutation result

With Graphcache, the cache is automatically updated with the mutation result

Every piece of data should have a __typename and id

const UpdateTodo = `
  mutation ($id: ID!, $title: String!) {
    updateTodo (id: $id, title: $title) {
      id
      title
    }
  }
`;

If the data doesn't have a reliable id

You can add it manually in the cache config

Or opt out of the cache for these values

const cacheExchangeConfig = {
  keys: {
    SomeFieldWithoutId: data => /* calculate id here based on parent */,
  },
}
const cacheExchangeConfig = {
  keys: {
    SomeFieldWithoutId: () => null,
  },
}

What if the mutation result doesn't return everything that gets updated?

You can also define manual update queries if you need to

import { cacheExchange } from "@urql/exchange-graphcache";

const cacheExchangeConfig = {
  updates: {
    updateToDoListComplete: (
      mutationResult: CompleteToDoType,
      _: any,
      cache: any,
    ) => {
      // if the user completed the last item in their ToDo list, remove it from their profile
      if (mutationResult.updateTodoListComplete.completedPercentage === 1) {
        cache.updateQuery({ query: currentUserQuery }, (data: any) => {
          data.currentUser.currentToDoList = null;
          return data;
        });
      }
    },
  }
}

const client = new Client({
  exchanges: [dedupExchange, cacheExchange(cacheExchangeConfig), fetchExchange],
});

Offline Support?

React Apollo (with apollo-cache-persist)

https://github.com/apollographql/apollo-cache-persist

urql (with graphcache)

https://formidable.com/open-source/urql/docs/graphcache/offline/

urql devtools

https://github.com/FormidableLabs/urql-devtools

Full React Native support 😍

  • see network requests
  • inspect the cache
  • execute queries

Setup

# npm
npm i @urql/devtools

# yarn
yarn add @urql/devtools

Install the exchange:

Add the exchange to your urql client:

import { createClient, defaultExchanges } from 'urql';
import { devtoolsExchange } from '@urql/devtools';

const client = createClient({
  url: 'http://localhost:3001/graphql',
  exchanges: [devtoolsExchange, ...defaultExchanges],
});
npx urql-devtools

Then to launch the dev tools:

Schema explorer

Query / Mutation timeline

Playground

In Summary

Apollo Client

 

first GraphQL client for React!

 

larger existing community

urql

 

fast issue resolution

 

Graphcache!!!!

 

built to be fully extensible

Using GraphQL with React Native is awesome!

Thank you!

https://formidable.com/open-source/urql

https://github.com/kadikraman/UrqlTest

React Native demo with urql

urql docs

https://github.com/FormidableLabs/urql-devtools

urql devtools

Apollo docs

https://www.apollographql.com/docs/react/api/core/ApolloClient

https://slides.com/kadikraman/rn-graphql-urql/fullscreen

Slides

@kadikraman

GraphQL and React Native - the urql edition

By Kadi Kraman

GraphQL and React Native - the urql edition

React Native EU 2020

  • 2,018