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