Logomobx-keystone
API Ref
IntroductionComparison with mobx-state-treeClass ModelsFunctional ModelsTree-Like StructureRoot StoresSnapshotsPatchesMaps, Sets, Dates
Action Middlewares
ContextsReferencesFrozen DataRuntime Type CheckingProperty TransformsDraftsSandboxesRedux Compatibility
Examples

Examples / Todo List

TODO
👀make mobx-keystone awesome!
👀spread the word
DONE
✔️buy some milk


Action log
Patch log
Generated immutable snapshot
{
  "todos": [
    {
      "text": "make mobx-keystone awesome!",
      "id": "e502cfd4-9ae2-4396-867f-654e454a8c34",
      "done": false,
      "$modelId": "4-B0LDq8OHw5jCqUvCpsKjwp7ChnLDqm3Ct1U=",
      "$modelType": "todoSample/Todo"
    },
    {
      "text": "spread the word",
      "id": "6afd393c-8c1a-407b-9fb3-ca3877ae7f92",
      "done": false,
      "$modelId": "5-B0LDq8OHw5jCqUvCpsKjwp7ChnLDqm3Ct1U=",
      "$modelType": "todoSample/Todo"
    },
    {
      "text": "buy some milk",
      "done": true,
      "id": "f3cbab61-ba0a-406b-8532-6fbebaa351e7",
      "$modelId": "6-B0LDq8OHw5jCqUvCpsKjwp7ChnLDqm3Ct1U=",
      "$modelType": "todoSample/Todo"
    }
  ],
  "$modelId": "7-B0LDq8OHw5jCqUvCpsKjwp7ChnLDqm3Ct1U=",
  "$modelType": "todoSample/TodoList"
}

examples/todoList/store.ts

import { computed } from "mobx"
import {
connectReduxDevTools,
model,
Model,
modelAction,
ModelAutoTypeCheckingMode,
registerRootStore,
setGlobalConfig,
tProp,
types,
} from "mobx-keystone"
import { v4 as uuidv4 } from "uuid"
// for this example we will enable runtime data checking even in production mode
setGlobalConfig({
modelAutoTypeChecking: ModelAutoTypeCheckingMode.AlwaysOn,
})
// the model decorator marks this class as a model, an object with actions, etc.
// the string identifies this model type and must be unique across your whole application
@model("todoSample/Todo")
export class Todo extends Model({
// here we define the type of the model data, which is observable and snapshottable
// and also part of the required initialization data of the model
// in this case we use runtime type checking,
id: tProp(types.string, () => uuidv4()), // an optional string that will use a random id when not provided
text: tProp(types.string), // a required string
done: tProp(types.boolean, false), // an optional boolean that will default to false
// if we didn't require runtime type checking we could do this
// id: prop(() => uuidv4())
// text: prop<string>(),
// done: prop(false)
}) {
// the modelAction decorator marks the function as a model action, giving it access
// to modify any model data and other superpowers such as action
// middlewares, replication, etc.
@modelAction
setDone(done: boolean) {
this.done = done
}
@modelAction
setText(text: string) {
this.text = text
}
}
@model("todoSample/TodoList")
export class TodoList extends Model({
// in this case the default uses an arrow function to create the object since it is not a primitive
// and we need a different array for each model instane
todos: tProp(types.array(types.model<Todo>(Todo)), () => []),
// if we didn't require runtime type checking
// todos: prop<Todo[]>(() => [])
}) {
// standard mobx decorators (such as computed) can be used as usual, since props are observables
@computed
get pending() {
return this.todos.filter(t => !t.done)
}
@computed
get done() {
return this.todos.filter(t => t.done)
}
@modelAction
add(todo: Todo) {
this.todos.push(todo)
}
@modelAction
remove(todo: Todo) {
const index = this.todos.indexOf(todo)
if (index >= 0) {
this.todos.splice(index, 1)
}
}
}
export function createRootStore(): TodoList {
// the parameter is the initial data for the model
const rootStore = new TodoList({
todos: [
new Todo({ text: "make mobx-keystone awesome!" }),
new Todo({ text: "spread the word" }),
new Todo({ text: "buy some milk", done: true }),
],
})
// although not strictly required, it is always a good idea to register your root stores
// as such, since this allows the model hook `onAttachedToRootStore` to work and other goodies
registerRootStore(rootStore)
// we can also connect the store to the redux dev tools
const remotedev = require("remotedev")
const connection = remotedev.connectViaExtension({
name: "Todo List Example",
})
connectReduxDevTools(remotedev, connection, rootStore)
return rootStore
}

examples/todoList/app.tsx

import { observer } from "mobx-react"
import React, { useState } from "react"
import { LogsView } from "./logs"
import { createRootStore, Todo, TodoList } from "./store"
// we use mobx-react to connect to the data, as it is usual in mobx
// this library is framework agnostic, so it can work anywhere mobx can work
// (even outside of a UI)
export const App = observer(() => {
const [rootStore] = useState(() => createRootStore())
return (
<>
<TodoListView list={rootStore} />
<br />
<LogsView rootStore={rootStore} />
</>
)
})
export const TodoListView = observer(({ list }: { list: TodoList }) => {
const [newTodo, setNewTodo] = React.useState("")
const renderTodo = (todo: Todo) => (
<TodoView
key={todo.id}
done={todo.done}
text={todo.text}
onClick={() => todo.setDone(!todo.done)}
onRemove={() => list.remove(todo)}
/>
)
return (
<div>
{list.pending.length > 0 && (
<>
<h5>TODO</h5>
{list.pending.map(t => renderTodo(t))}
</>
)}
{list.done.length > 0 && (
<>
<h5>DONE</h5>
{list.done.map(t => renderTodo(t))}
</>
)}
<br />
<input
value={newTodo}
onChange={ev => {
setNewTodo(ev.target.value || "")
}}
placeholder="I will..."
/>
<button
onClick={() => {
list.add(new Todo({ text: newTodo }))
setNewTodo("")
}}
>
Add todo
</button>
</div>
)
})
function TodoView({
done,
text,
onClick,
onRemove,
}: {
done: boolean
text: string
onClick(): void
onRemove(): void
}) {
return (
<div style={{ cursor: "pointer" }}>
<span
onClick={onClick}
style={{
textDecoration: done ? "line-through" : "inherit",
}}
>
<span
style={{
display: "inline-block",
width: "1.5rem",
textAlign: "center",
marginRight: 8,
}}
>
{done ? "✔️" : "👀"}
</span>
{text}
{}
</span>
<span onClick={onRemove} style={{ marginLeft: 16 }}>
</span>
</div>
)
}

examples/todoList/logs.tsx

import { ActionCall, getSnapshot, onActionMiddleware, onPatches, Patch } from "mobx-keystone"
import { observer, useLocalStore } from "mobx-react"
import React, { useEffect } from "react"
import { TodoList } from "./store"
// this is just for the client/server demo
export const cancelledActionSymbol = Symbol("cancelledAction")
interface ExtendedActionCall extends ActionCall {
cancelled: boolean
}
export const LogsView = observer((props: { rootStore: TodoList }) => {
const data = useLocalStore(() => ({
actions: [] as ExtendedActionCall[],
patchesList: [] as Patch[][],
addAction(actionCall: ExtendedActionCall) {
this.actions.push(actionCall)
},
addPatches(patches: Patch[]) {
this.patchesList.push(patches)
},
}))
useEffect(() => {
// we can use action middlewares for several things
// in this case we will keep a log of the actions done over the todo list
const disposer = onActionMiddleware(props.rootStore, {
onFinish(actionCall, ctx) {
const extendedActionCall: ExtendedActionCall = {
...actionCall,
cancelled: !!ctx.data[cancelledActionSymbol],
}
data.addAction(extendedActionCall)
},
})
return disposer
}, [props.rootStore])
useEffect(() => {
// also it is possible to get a list of changes in the form of patches,
// even with inverse patches to undo the changes
const disposer = onPatches(props.rootStore, (patches, _inversePatches) => {
data.addPatches(patches)
})
return disposer
})
// we can convert any model (or part of it) into a plain JS structure
// with it we can:
// - serialize to later deserialize it with `fromSnapshot`
// - pass it to non mobx-friendly components
// snapshots respect immutability, so if a subobject is changed
// its refrence will be kept
const rootStoreSnapshot = getSnapshot(props.rootStore)
return (
<>
<PreSection title="Action log">
{data.actions.map((action, index) => (
<ActionCallToText actionCall={action} key={index} />
))}
</PreSection>
<PreSection title="Patch log">{data.patchesList.map(patchesToText)}</PreSection>
<PreSection title="Generated immutable snapshot">
{JSON.stringify(rootStoreSnapshot, null, 2)}
</PreSection>
</>
)
})
function ActionCallToText(props: { actionCall: ExtendedActionCall }) {
const actionCall = props.actionCall
const args = actionCall.args.map(arg => JSON.stringify(arg)).join(", ")
const path = actionCall.targetPath.join("/")
let text = `[${path}] ${actionCall.actionName}(${args})`
if (actionCall.cancelled) {
return (
<>
<span style={{ textDecoration: "line-through" }}>{text}</span>{" "}
<span>(cancelled and sent to server)</span>
<br />
</>
)
}
return (
<>
<span>{text}</span>
<br />
</>
)
}
function patchToText(patch: Patch) {
const path = patch.path.join("/")
let str = `[${path}] ${patch.op}`
if (patch.op !== "remove") {
str += " -> " + JSON.stringify(patch.value)
}
return str + "\n"
}
function patchesToText(patches: Patch[]) {
return patches.map(patchToText)
}
function PreSection(props: { title: string; children: React.ReactNode }) {
return (
<>
<h5>{props.title}</h5>
<pre style={{ fontSize: 10, whiteSpace: "pre-wrap" }}>{props.children}</pre>
</>
)
}