Kadi Kraman
@kadikraman
Engineering Manager
Formidable
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)
(using Apollo Client)
(using urql)
various RN projects with and without GraphQL
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
# 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
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>
);
};
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>
));
}
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
???
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/
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],
});
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],
});
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,
},
},
},
};
};
[---]
}
Work in progress!
Check my twitter (@kadikraman) for an update after this talk!
Default cache, good for simple applications
Enabling fetching the data from cache instead of querying anew each time
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
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,
],
});
More sophisticated cache, handles interdependencies!
import { cacheExchange } from "@urql/exchange-graphcache";
const client = new Client({
exchanges: [dedupExchange, cacheExchange({}), fetchExchange],
});
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);
};
in React Apollo
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]
}
}
});
}
};
Every piece of data should have a __typename and id
const UpdateTodo = `
mutation ($id: ID!, $title: String!) {
updateTodo (id: $id, title: $title) {
id
title
}
}
`;
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,
},
}
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],
});
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/
https://github.com/FormidableLabs/urql-devtools
Full React Native support 😍
# 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
Schema explorer
Query / Mutation timeline
Playground
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!
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