MobX Depot Logo

MobX Depot

Docs

Get started
Models
Queries
Mutations
Root store
Caching
React hooks

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? The set method comes from TodoBaseModel 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.ts
import { 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 into useMutation (useMutation(todo.save)). This will cause the method to lose its context, thus any usage of this 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 by awaiting the mutate method, it's expected that you're handling the result on your own.