Data that enters your application begins as plain, immutable data.

For example, when you dispatch a query (or mutation) the data returned will reflect the remote state of your objects.

{
  user {
    __typename
    id
    firstName
    lastName
  }
}

This will return:

{
  "user": {
    "__typename": "User",
    "id": 1,
    "firstName": "John",
    "lastName": "Smith"
  }
}

You’ll receive access to a user object that you cannot change. You can pass this object around, and if a query or mutation causes an update that user, it will update in-place.

Lifecycle

  1. Data is introduced from server responses to queries/mutations as immutable objects
  2. Data is mutated via factories, while the original data isn’t changed
  3. Mutated data is passed to mutations
  4. Data re-enters the application from a mutation response which causes updates to the original immutable objects
    1. This seems to undermine immutability, but if mobx-depot is the only one who can do this then that’s “okay”

Factories

mobx-depot provides a utility function called createFactory that allows you to opt into extended functionality when you need it:

import { UserQuery, ObjectTypes } from '~/models/depot'

const userQuery = new UserQuery();
const data = await userQuery.dispatch();

const { user } = data;

// user is a plain immutable object. Let's create a factory so we can extend it

import { createFactory } from 'mobx-depot';

const EditableUser = createFactory<ObjectTypes.User>({
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  setFirstName(firstName: string) {
    this.firstName = firstName;
  }
});

// Adds our getter and action
const editable = EditableUser(user);

editable.setFirstName('Jeff');
editable.fullName; // -> 'Jeff Smith';

user.fullName; // TS error, otherwise `undefined`
user.firstName; // -> still 'John'

By default, whenever you pass the same object through a factory in different places, you’ll end up with the same extended object:

import { ObjectTypes } from '~/models/depot';
import { EditableUser } from '~/models/factories';

type UserPreviewProps = {
  user: ObjectTypes.User;
}

const UserPreview = (props: UserPreviewProps) => {
  const { user } = props;

	const editable = EditableUser(user);

  return (
    <span>{editable.firstName}</span>
  )
}

type EditUserFormProps = {
  user: ObjectTypes.User;
}

const EditUserForm = (props: EditUserFormProps) => {
  const { user } = props;

	const editable = EditableUser(user);

  return (
    <input
			value={editable.firstName}
			onChange={e => editable.setFirstName(e.target.value)}
		/>
  )
}

Changes to editable in EditUserForm cause UserPreview to update as well.

If this is not desirable for your use-case, you can use the branch utility to create explicit deviations:

import { branch } from 'mobx-depot';
import { ObjectTypes } from '~/models/depot';
import { EditableMenuItem } from '~/models/factories';

type MenuPreviewProps = {
  menuItems: ObjectTypes.MenuItem[];
}

const MenuPreview = (props: UserPreviewProps) => {
  const { menuItems } = props;

	const previewItems = menuItems.map(item => {
    return EditableMenuItem(item, 'preview');
	});

  return (
    <span>
			{/* do stuff with preview items */}
		</span>
  )
}

export const EditableMenuItem = createFactory<ObjectTypes.MenuItem>({
  setUrl() {

  },
  // ... 
  set(data) {
    Object.assign(this, data);
  }
})

type EditMenuItemFormProps = {
  menuItem: ObjectTypes.MenuItem;
}

const EditMenuItemForm = (props: EditMenuItemFormProps) => {
  const { menuItem } = props;

	const editable = EditableMenuItem(menuItem, 'edit');

	const commit = () => {
    const preview = EditableMenuItem(menuItem, 'preview');

		preview.commit(editable);
  }

  return (
		<>
			<button onClick={commit}>Done</button>
	    <input
				value={editable.firstName}
				onChange={e => editable.setFirstName(e.target.value)}
			/>
		</>
  )
}

In this case, changes to editable in EditUserForm will NOT cause UserPreview to update since they are on different branches.