A drop-in replacement for @apollo/client with automatic cache updates. It will update apollo cache based on a mutation or subscription result.
npm i @jimmyn/apollo-client @apollo/client --save
or
yarn add @jimmyn/apollo-client @apollo/client
import React from 'react';
import {render} from 'react-dom';
import {ApolloClient, InMemoryCache} from '@jimmyn/apollo-client';
const client = new ApolloClient({
uri: 'localhost:8080',
cache: new InMemoryCache()
});
const App = () => (
<ApolloProvider client={client}>
<div>
<h2>My first Apollo app 🚀</h2>
</div>
</ApolloProvider>
);
render(<App />, document.getElementById('root'));
This package
extends useMutation options
allowing to update cached queries in one line of code instead of writing complex update
functions.
For example this code
import React from 'react';
import {useMutation, useQuery} from '@jimmyn/apollo-client';
import {createTodoMutation, todosQuery} from './api/operations';
import {TodosList} from './TodosList';
export const Todos = () => {
const {data} = useQuery(todosQuery);
const todos = data?.todos || [];
const [createTodo] = useMutation(createTodoMutation, {
updateQuery: todosQuery // <== pass a gql query you want to update
});
const handleCreateTodo = () => {
return createTodo({
variables: {
task: 'New todo',
createdAt: new Date().toISOString()
}
});
};
return (
<div>
<button onClick={handleCreateTodo}>Create todo</button>
<TodosList todos={todos} />
</div>
);
};
is equivalent to
import React from 'react';
import {useMutation, useQuery} from '@apollo/react-hooks';
import {createTodoMutation, todosQuery} from './api/operations';
import {TodosList} from './TodosList';
export const Todos = () => {
const {data} = useQuery(todosQuery);
const todos = data?.todos || [];
const [createTodo] = useMutation(createTodoMutation);
const handleCreateTodo = () => {
return createTodo({
variables: {
task: 'New todo',
createdAt: new Date().toISOString()
},
update: (proxy, {data}) => {
const newTodo = data.createTodo;
try {
const cache = proxy.readQuery({query: todosQuery});
proxy.writeQuery({
query: todosQuery,
data: {
todos: [...cache.todos, newTodo]
}
});
} catch (error) {
console.log(error);
}
}
});
};
return (
<div>
<button onClick={handleCreateTodo}>Create todo</button>
<TodosList todos={todos} />
</div>
);
};
And this code
import React from 'react';
import {useMutation} from '@jimmyn/apollo-client';
import {Todo} from './api/generated';
import {deleteTodoMutation, todosQuery, updateTodoMutation} from './api/operations';
type Props = {
todo: Todo;
};
export const Todo: React.FC<Props> = ({todo}) => {
const [deleteTodo] = useMutation(deleteTodoMutation, {
updateQuery: todosQuery,
// to delete an item we need to provide it's id
// if our api simply returns true when item is deleted
// we need to return an id explicitly
mapResultToUpdate: data => todo
});
const [updateTodo] = useMutation(updateTodoMutation);
const handleDeleteTodo = () => {
return deleteTodo({
variables: {id: todo.id}
});
};
const handleUpdateTodo = () => {
return updateTodo({
variables: {id: todo.id, done: !todo.done}
});
};
return (
<li>
<input type="checkbox" checked={todo.done} onChange={handleUpdateTodo} />
{todo.task}
<button onClick={handleDeleteTodo}>delete</button>
</li>
);
};
is equivalent to
import React from 'react';
import {useMutation} from '@apollo/react-hooks';
import {Todo} from './api/generated';
import {deleteTodoMutation, todosQuery, updateTodoMutation} from './api/operations';
type Props = {
todo: Todo;
};
export const Todo: React.FC<Props> = ({todo}) => {
const [deleteTodo] = useMutation(deleteTodoMutation);
const [updateTodo] = useMutation(updateTodoMutation);
const handleDeleteTodo = () => {
return deleteTodo({
variables: {id: todo.id},
update: proxy => {
try {
const cache = proxy.readQuery({query: todosQuery});
proxy.writeQuery({
query: todosQuery,
data: {
todos: cache.todos.filter(item => item.id !== todo.id)
}
});
} catch (error) {
console.log(error);
}
}
});
};
const handleUpdateTodo = () => {
// apollo client is clever enough to update an item in cache
// although if you want to update an item with different type you'll have to write
// a manual update function
return updateTodo({
variables: {id: todo.id, done: !todo.done}
});
};
return (
<li>
<input type="checkbox" checked={todo.done} onClick={handleUpdateTodo} />
{todo.task}
<button onClick={handleDeleteTodo}>delete</button>
</li>
);
};
Option | Description | Default |
---|---|---|
updateQuery |
A graphql query (wrapped in gql tag) that should be updated. You can pass query directly or specify it with variables {query: todosQuery, variables: {limit: 10}} |
|
updatePath |
Overrides a path inside the query where the item should be updated. For example if your query has structure like {items: [...], total: 10} , you can specify updatePath: ['items'] . In most cases it is automatically detected. Pass [] to update data in the root query object |
|
idField |
Unique field that is used to find the item in cache. It should be present in the mutation response | id |
operationType |
Indicates what type of the operation should be performed e.g. add/remove/update item. By default operation type is automatically detected from mutation name e.g. createTodo will result in OperationTypes.ADD . |
OperationTypes.AUTO |
mapResultToUpdate |
A function that receives mutation result and returns an updated item. Function result should contain at least an id field |
Other useMutation
hook options
Offline options can be passed to the useMutation
hook or to the mutation function directly.
const [deleteTodo] = useMutation(deleteTodoMutation, {
updateQuery: todosQuery,
mapResultToUpdate: data => todo
});
const handleDeleteTodo = () => {
return deleteTodo({
variables: {id: todo.id}
});
};
is the same as
const [deleteTodo] = useMutation(deleteTodoMutation);
const handleDeleteTodo = () => {
return deleteTodo({
variables: {id: todo.id},
updateQuery: todosQuery,
mapResultToUpdate: data => todo
});
};
useSubscription
accepts the same offline options as useMutation
useSubscription(onTodoUpdate, {updateQuery: todosQuery});
Other useSubscription
hook options
Default configurations can be customized by calling setOfflineConfig
import {setOfflineConfig} from '@jimmyn/apollo-client';
setOfflineConfig({
getIdFieldFromObject(item: any) {
switch (item.__typename) {
case 'Todo':
return 'id';
case 'User':
return 'user_id';
}
}
});
Option | Description | Default |
---|---|---|
idField |
Unique field that is used to find the item in cache. It should be present in the mutation response | id |
getIdFieldFromObject |
A function that receives updated item and returns an id field name. If defined it will tke precedence over idField |
|
prefixesForRemove |
A list of mutation name prefixes that will result in remove operation | prefixesForRemove |
prefixesForUpdate |
A list of mutation name prefixes that will result in update operation | prefixesForUpdate |
prefixesForAdd |
A list of mutation name prefixes that will result in add operation | prefixesForAdd |
This package also exposes updateApolloCache
function directly, that can be used to build custom
implementations
Example
import {updateApolloCache} from '@jimmyn/apollo-client';
const newTodo = {
__typename: 'Todo',
id: 1,
task: 'New todo',
done: false,
createdAt: new Date().toISOString()
};
updateApolloCache({
client,
data: {createTodo: newTodo},
updateQuery: todosQuery
});
Function signature
type OfflineOptions<TData> = {
updateQuery?: QueryWithVariables | DocumentNode;
idField?: string;
operationType?: OperationTypes;
mapResultToUpdate?(data: NonNullable<TData>): Item;
};
type UpdateCacheOptions<TData = any> = OfflineOptions<TData> & {
client: ApolloClient<any> | DataProxy;
data: TData;
};
const updateApolloCache: <TData = any>({
client,
data,
idField,
updateQuery,
operationType,
mapResultToUpdate
}: UpdateCacheOptions<TData>) => void;
This package is based on Amplify Offline Helpers