From 9577fc7162e284a471439fc0a1c1fe45b7b6b497 Mon Sep 17 00:00:00 2001 From: Tyler Scott Williams Date: Fri, 22 Mar 2024 14:00:38 -0600 Subject: [PATCH] docs: add recipe for auto-generated property setters (#2169) --- .../auto-generated-property-setter-actions.md | 137 ++++++++++++++++++ website/i18n/en.json | 4 + website/sidebars.json | 17 +-- 3 files changed, 146 insertions(+), 12 deletions(-) create mode 100644 docs/recipes/auto-generated-property-setter-actions.md diff --git a/docs/recipes/auto-generated-property-setter-actions.md b/docs/recipes/auto-generated-property-setter-actions.md new file mode 100644 index 000000000..7f27dfb5c --- /dev/null +++ b/docs/recipes/auto-generated-property-setter-actions.md @@ -0,0 +1,137 @@ +--- +id: auto-generated-property-setter-actions +title: Auto-Generated Property Setter Actions +--- + +This recipe was [originally developed by Infinite Red](https://shift.infinite.red/a-mobx-state-tree-shortcut-for-setter-actions-ac88353df060). + +If you want to modify your MobX-State-Tree model properties, you usually have to write one setter per-property. In a model with two fields, that looks like this: + +```ts +import { types } from "mobx-state-tree" + +const UserModel = types + .model("User", { + name: types.string, + age: types.number + }) + .actions((self) => ({ + setName(newName: string) { + self.name = newName + }, + setAge(newAge: number) { + self.age = newAge + } + })) +``` + +As your model grows in size and complexity, these setter actions can be tedious to write, and increase your source file size, making it harder to read through the actual logic of your model. + +You can write a generic action in your model, like this: + +```ts +import { types, SnapshotIn } from "mobx-state-tree" +const UserModel = types + .model("User", { + name: types.string, + age: types.number + }) + .actions((self) => ({ + setProp, V extends SnapshotIn[K]>( + field: K, + newValue: V + ) { + self[field] = newValue + } + })) +const user = UserModel.create({ name: "Jamon", age: 40 }) +user.setProp("name", "Joe") // all good! +// typescript will error, like it's supposed to +user.setProp("age", "shouldn't work") +``` + +Or, if you want to extract that for easier reuse across different models, you can write a helper, like this: + +```ts +import { IStateTreeNode, SnapshotIn } from "mobx-state-tree" + +// This custom type helps TS know what properties can be modified by our returned function. It excludes actions and views, but still correctly infers model properties for auto-complete and type safety. +type OnlyProperties = { + [K in keyof SnapshotIn]: K extends keyof T ? T[K] : never +} + +/** + * If you include this in your model in an action() block just under your props, + * it'll allow you to set property values directly while retaining type safety + * and also is executed in an action. This is useful because often you find yourself + * making a lot of repetitive setter actions that only update one prop. + * + * E.g.: + * + * const UserModel = types.model("User") + * .props({ + * name: types.string, + * age: types.number + * }) + * .actions(withSetPropAction) + * + * const user = UserModel.create({ name: "Jamon", age: 40 }) + * + * user.setProp("name", "John") // no type error + * user.setProp("age", 30) // no type error + * user.setProp("age", "30") // type error -- must be number + */ +export const withSetPropAction = (mstInstance: T) => ({ + setProp, V extends SnapshotIn[K]>(field: K, newValue: V) { + ;(mstInstance as T & OnlyProperties)[field] = newValue + } +}) +``` + +You can use the helper in a model like so: + +```ts +import { t } from "mobx-state-tree" +import { withSetPropAction } from "./withSetPropAction" + +const Person = t + .model("Person", { + name: t.string + }) + .views((self) => ({ + get lowercaseName() { + return self.name.toLowerCase() + } + })) + .actions((self) => ({ + setName(name: string) { + self.name = name + } + })) + .actions(withSetPropAction) + +const you = Person.create({ + name: "your name" +}) + +you.setProp("name", "Another Name") + +// These will all trigger runtime errors. They are included to demonstrate TS support for +// withSetPropAction. +try { + // @ts-expect-error - this should error because it's the wrong type for name. + you.setProp("name", 123) + // @ts-expect-error - this should error since 'nah' is not a property. + you.setProp("nah", 123) + // @ts-expect-error - we cannot set views like we can with properties. + you.setProp("lowercaseName", "your name") + // @ts-expect-error - we cannot set actions like we can with properties. + you.setProp("setName", "your name") +} catch (e) { + console.error(e) +} +``` + +[See this working in CodeSandbox](https://codesandbox.io/p/sandbox/set-prop-action-ts-fix-p5psk7?file=%2Fsrc%2Findex.ts%3A9%2C23). + +This is a type-safe way to reduce boilerplate and make your MobX-State-Tree models more readable. diff --git a/website/i18n/en.json b/website/i18n/en.json index 477333037..2bd64f803 100644 --- a/website/i18n/en.json +++ b/website/i18n/en.json @@ -191,6 +191,9 @@ "overview/api": { "title": "API overview" }, + "recipes/auto-generated-property-setter-actions": { + "title": "Auto-Generated Property Setter Actions" + }, "tips/circular-deps": { "title": "Handle circular dependencies between files and types using `late`", "sidebar_label": "Circular dependencies" @@ -228,6 +231,7 @@ "API Overview": "API Overview", "Tips": "Tips", "Compare": "Compare", + "Recipes": "Recipes", "Interfaces": "Interfaces" } }, diff --git a/website/sidebars.json b/website/sidebars.json index e8e4b409b..7f93a502e 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -24,11 +24,7 @@ "concepts/reconciliation", "concepts/volatiles" ], - "API Overview": [ - "overview/types", - "overview/api", - "overview/hooks" - ], + "API Overview": ["overview/types", "overview/api", "overview/hooks"], "Tips": [ "tips/resources", "tips/contributing", @@ -39,14 +35,11 @@ "tips/snapshots-as-values", "tips/more-tips" ], - "Compare": [ - "compare/context-reducer-vs-mobx-state-tree" - ] + "Compare": ["compare/context-reducer-vs-mobx-state-tree"], + "Recipes": ["recipes/auto-generated-property-setter-actions"] }, "mobx-state-tree": { - "Introduction": [ - "API/index" - ], + "Introduction": ["API/index"], "Interfaces": [ "API/interfaces/customtypeoptions", "API/interfaces/functionwithflag", @@ -78,4 +71,4 @@ "API/interfaces/unionoptions" ] } -} \ No newline at end of file +}