React hooks
mobx-depot
provides a set of React hooks for a variety of use-cases.
Autogenerated query and mutation hooks
For each query and mutation a hook is generated. That is, unless you provide --react false
when using the generate
CLI.
For example, given the following schema:
type Todo { id: ID! title: String! content: String! completedAt: DateTime}type Query { todos: [Todo!]!}
You can use the hook like so:
import { useTodosQuery } from '~/models/depot';// ... in some component:const { loading, data, error, dispatch } = useTodosQuery( // Query variables (if applicable) { sortBy: "completedAt" }, todo => todo.title.content.completedAt, { cachePolicy: 'cache-first', lazy: true } // Optional! Combines both `useQuery` and `TodosQuery` options)
This can reduce syntax verbosity in your components. However, if you want finer control, you can always import a
mutation or query class, as well as either useQuery
or useMutation
from mobx-depot
and handle it manually.
See the docs below for details on how to use each hook.
useQuery
Given the following schema:
type Todo { id: ID! title: String! content: String!}type Query { todos: [Todo!]!}
You can import useQuery
and use it like this:
import { observer } from 'mobx-react-lite';import { useQuery } from "mobx-depot";import { TodosQuery } from '~/models/depot';export const TodoList = observer(() => { const { data, loading } = useQuery(() => new TodosQuery(todo => todo.title.content)); return ( <div className="TodoList"> {loading && <div>Loading...</div>} {data?.todos.map(todo => ( // NOTE! `todo` is a `TodoModel` :D <div className="Todo"> <h1>{todo.title}</h1> <p>{todo.content}</p> </div> ))} </div> )});
You can also make queries lazy if you want to dispatch them yourself:
const { data, loading, dispatch } = useQuery(() => new TodosQuery(todo => todo.title.content), { lazy: true });dispatch();
If you want to provide a dynamic set of variables when you dispatch your query, you can provide them to the dispatch
function returned from the hook:
const { data, loading, dispatch } = useQuery( () => new TodosQuery({ sort: 'created_at' }, todo => todo.title.content), { lazy: true });dispatch({ sort: 'completed_at' });// If you call dispatch later, it will use the variables it was given upon initialization!dispatch(); // Uses { sort: 'created_at' }
Same goes for useMutation
described below.
useMutation
Given the following schema:
type Todo { id: ID! title: String! content: String!}input UpdateTodoInput { title: String content: String}type Mutation { updateTodo(id: ID!, todo: UpdateTodoInput!): Todo}
This generates a TodoModel
as well as an UpdateTodoMutation
. Import useMutation
and use it like so:
import { observer } from 'mobx-react-lite';import { useMutation } from "mobx-depot";import { TodoModel } from '~/models/TodoModel';import { UpdateTodoMutation } from '~/models/depot';type EditTodoFormProps = { todo: TodoModel;}export const EditTodoForm = observer(({ todo }: EditTodoFormProps) => { const { data, loading, dispatch } = useMutation( () => new UpdateTodoMutation( { id: todo.id, todo: { title: todo.title, content: todo.content } }, todo => todo.title.content, ), { onSuccess: (data) => { console.log(data); alert('Updated todo!') }, } ); return ( <div className="EditTodoForm"> {loading && <p>Loading...</p>} <input value={todo.title} onChange={e => todo.set('title', e.target.value)} /> <textarea value={todo.content} onChange={e => todo.set('content', e.target.value)} /> <button onClick={dispatch}>Save</button> </div> )});
Note: See our usage of
todo.set
? Theset
method comes fromTodoBaseModel
as a built-in action.
Provide variables dynamically
Just like with useQuery
, you can provide variables
when you call dispatch
:
export const TodoList = observer(({ todos }) => { const { data, loading, dispatch } = useMutation( () => new DeleteTodoMutation( null, // At initialization, we don't know what the variables are, and we can't provide sensible defaults. todo => todo.title.content, ), { onSuccess: (data) => { console.log(data); alert('Deleted todo!') }, } ); return ( <div className="TodoList"> {loading && <div>Loading...</div>} {todos.map(todo => ( <div className="Todo"> <h1>{todo.title}</h1> <p>{todo.content}</p> <button onClick={() => dispatch({ id: todo.id })}>Delete</button> </div> ))} </div> )});
As you can see, this is helpful when you need to call dispatch
when looping through data.
Best practices with Mutation
classes
I heavily recommend putting the UpdateTodoMutation
instantiation within a method on your model. This keeps components
dumb!
// src/models/TodoModel.tsimport { UpdateTodoMutation } from '~/models/depot';export class TodoModel extends TodoBaseModel { constructor(init: Partial<TodoBaseModel> = {}) { super(init); makeModelObservable(this); } get isComplete() { return !!this.completedAt; } save() { // NOTE: As-is, this does not dispatch the operation. By returning the mutation we're expecting // the component to dispatch it via `useMutation`. return new UpdateTodoMutation( { title: this.title, content: this.content }, todo => todo.title.content ); }}
// src/components/EditTodoForm.tsx...export const EditTodoForm = observer(({ todo }: EditTodoFormProps) => { const { data, loading, dispatch } = useMutation(() => todo.save(), { onSuccess: (data) => { console.log(data); alert('Updated todo!') }, }); return ( <div className="EditTodoForm"> {loading && <p>Loading...</p>} <input value={todo.title} onChange={e => todo.set('title', e.target.value)} /> <textarea value={todo.content} onChange={e => todo.set('content', e.target.value)} /> <button onClick={dispatch}>Save</button> </div> )});
Note: Do not pass
todo.save
directly intouseMutation
(useMutation(todo.save)
). This will cause the method to lose its context, thus any usage ofthis
will inevitably fail. Use an arrow function, or bind the method to the instance. (i.e.() => todo.save()
,todo.save.bind(todo)
)
You can dispatch the Mutation
if your model needs to handle the response, however it gets a bit interesting:
export class TodoModel extends TodoBaseModel { constructor(init: Partial<TodoBaseModel> = {}) { super(init); makeModelObservable(this); } get isComplete() { return !!this.completedAt; } save() { const mutation = new UpdateTodoMutation( { title: this.title, content: this.content }, todo => todo.title.content ); mutation.dispatch().then(todo => { // Handle the result }); return mutation; }}
Note: You do not need to manually update your instance. If your instance knows its ID (which is automatically selected by generated queries/mutations,) and the GraphQL query returns that same ID in the payload the
RootStore
will automatically reconcile the data in any existing instances.
As you can see, the save
method stays synchronous so it can return the mutation
to the useMutation
hook.
This allows your components to stay updated when the state of the mutation
changes.
However, if you don't plan on handling the intermediate state of the mutation, you can use async/await
:
export class TodoModel extends TodoBaseModel { constructor(init: Partial<TodoBaseModel> = {}) { super(init); makeModelObservable(this); } get isComplete() { return !!this.completedAt; } async save() { const mutation = new UpdateTodoMutation( { title: this.title, content: this.content }, todo => todo.title.content ); await mutation.dispatch(); }}
Note:
useMutation
does not work with async functions. Its sole purpose is to handle the lifecycle of a mutation. If you're handling it yourself byawait
ing themutate
method, it's expected that you're handling the result on your own.