@koordinates/xstate-tree 2.0.9 → 2.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,82 +15,113 @@ While xstate-tree manages your application state, it does not have a mechanism f
15
15
 
16
16
  At Koordinates we use xstate-tree for all new UI development. Our desktop application, built on top of [Kart](https://kartproject.org/) our Geospatial version control system, is built entirely with xstate-tree using GraphQL for global state.
17
17
 
18
- A minimal example of a single machine tree
18
+ A minimal example of a single machine tree ([CodeSandbox](https://codesandbox.io/s/xstate-tree-b0el6e-forked-4i6txh?file=/src/index.tsx)):
19
19
 
20
20
  ```tsx
21
21
  import React from "react";
22
22
  import { createRoot } from "react-dom/client";
23
23
  import { createMachine } from "xstate";
24
24
  import { assign } from "@xstate/immer";
25
- import { buildSelectors, buildActions, buildView, buildXstateTreeMachine, buildRootComponent } from "@koordinates/xstate-tree";
26
-
27
- type Events = { type: "SWITCH_CLICKED" } | { type: "INCREMENT", amount: number };
25
+ import {
26
+ buildSelectors,
27
+ buildActions,
28
+ buildView,
29
+ buildXStateTreeMachine,
30
+ buildRootComponent
31
+ } from "@koordinates/xstate-tree";
32
+
33
+ type Events =
34
+ | { type: "SWITCH_CLICKED" }
35
+ | { type: "INCREMENT"; amount: number };
28
36
  type Context = { incremented: number };
29
37
 
30
38
  // If this tree had more than a single machine the slots to render child machines into would be defined here
31
39
  const slots = [];
32
40
 
33
41
  // A standard xstate machine, nothing extra is needed for xstate-tree
34
- const machine = createMachine<Context, Events>({
35
- id: "root",
36
- initial: "inactive",
37
- context: {
38
- incremented: 0,
39
- },
40
- states: {
41
- inactive: {
42
- on: {
43
- SWITCH_CLICKED: "active",
44
- },
42
+ const machine = createMachine<Context, Events>(
43
+ {
44
+ id: "root",
45
+ initial: "inactive",
46
+ context: {
47
+ incremented: 0
45
48
  },
46
- active: {
47
- on: {
48
- SWITCH_CLICKED: "idle",
49
- INCREMENT: { actions: "increment" },
49
+ states: {
50
+ inactive: {
51
+ on: {
52
+ SWITCH_CLICKED: "active"
53
+ }
50
54
  },
51
- },
55
+ active: {
56
+ on: {
57
+ SWITCH_CLICKED: "inactive",
58
+ INCREMENT: { actions: "increment" }
59
+ }
60
+ }
61
+ }
52
62
  },
53
- }, {
54
- actions: {
55
- increment: assign((context, event) => {
56
- context.incremented += event.amount;
57
- }),
58
- },
59
- });
63
+ {
64
+ actions: {
65
+ increment: assign((context, event) => {
66
+ if (event.type !== "INCREMENT") {
67
+ return;
68
+ }
69
+
70
+ context.incremented += event.amount;
71
+ })
72
+ }
73
+ }
74
+ );
60
75
 
61
76
  // Selectors to transform the machines state into a representation useful for the view
62
77
  const selectors = buildSelectors(machine, (ctx, canHandleEvent) => ({
63
- canIncrement: canHandleEvent({type: "INCREMENT", count: 1 }),
78
+ canIncrement: canHandleEvent({ type: "INCREMENT", amount: 1 }),
64
79
  showSecret: ctx.incremented > 10,
65
- count: ctx.incremented,
80
+ count: ctx.incremented
66
81
  }));
67
82
 
68
83
  // Actions to abstract away the details of sending events to the machine
69
- const actions = buildActions(machine, actions, (send, selectors) => ({
84
+ const actions = buildActions(machine, selectors, (send, selectors) => ({
70
85
  increment(amount: number) {
71
- send({ type: "INCREMENT", amount: selectors.count > 4 ? amount * 2 : amount });
86
+ send({
87
+ type: "INCREMENT",
88
+ amount: selectors.count > 4 ? amount * 2 : amount
89
+ });
72
90
  },
73
91
  switch() {
74
92
  send({ type: "SWITCH_CLICKED" });
75
- },
93
+ }
76
94
  }));
77
95
 
78
96
  // A view to bring it all together
79
97
  // the return value is a plain React view that can be rendered anywhere by passing in the needed props
80
98
  // the view has no knowledge of the machine it's bound to
81
- const view = buildView(machine, actions, selectors, slots, ({ actions, selectors, inState }) => {
82
- return (
83
- <div>
84
- <button onClick={() => actions.switch()}>{inState("active") ? "Deactivate" : "Activate"}</button>
85
- <p>Count: {selectors.count}</p>
86
- <button onClick={() => actions.increment(1)} disabled={!selectors.canIncrement}>Increment</button>
87
- {selectors.showSecret && <p>The secret password is hunter2</p>}
88
- </div>
89
- );
90
- });
99
+ const view = buildView(
100
+ machine,
101
+ selectors,
102
+ actions,
103
+ slots,
104
+ ({ actions, selectors, inState }) => {
105
+ return (
106
+ <div>
107
+ <button onClick={() => actions.switch()}>
108
+ {inState("active") ? "Deactivate" : "Activate"}
109
+ </button>
110
+ <p>Count: {selectors.count}</p>
111
+ <button
112
+ onClick={() => actions.increment(1)}
113
+ disabled={!selectors.canIncrement}
114
+ >
115
+ Increment
116
+ </button>
117
+ {selectors.showSecret && <p>The secret password is hunter2</p>}
118
+ </div>
119
+ );
120
+ }
121
+ );
91
122
 
92
123
  // Stapling the machine, selectors, actions, view, and slots together
93
- const RootMachine = buildXstateTreeMachine(machine, {
124
+ const RootMachine = buildXStateTreeMachine(machine, {
94
125
  selectors,
95
126
  actions,
96
127
  view,
@@ -100,12 +131,13 @@ const RootMachine = buildXstateTreeMachine(machine, {
100
131
  // Build the React host for the tree
101
132
  const XstateTreeRoot = buildRootComponent(RootMachine);
102
133
 
103
-
104
134
  // Rendering it with React
105
135
  const ReactRoot = createRoot(document.getElementById("root"));
106
136
  ReactRoot.render(<XstateTreeRoot />);
107
137
  ```
108
138
 
139
+ A more complicated todomvc [example](https://github.com/koordinates/xstate-tree/tree/master/examples/todomvc)
140
+
109
141
  ## Overview
110
142
 
111
143
  Each machine that forms the tree representing your UI has an associated set of selector, action, view functions, and "slots"
package/lib/builders.js CHANGED
@@ -45,8 +45,8 @@ export function buildSelectors(__machine, selectors) {
45
45
  * - `send` - the interpreters send function, which can be used to send events to the machine
46
46
  * - `selectors` - the output of the selectors function from {@link buildSelectors}
47
47
  *
48
- * The resulting action function has memoization. It will return the same value until the
49
- * selectors reference changes or the send reference changes
48
+ * The resulting action function will only be called once per invocation of a machine.
49
+ * The selectors are passed in as a proxy to always read the latest selector value
50
50
  *
51
51
  * @param machine - The machine to create the actions for
52
52
  * @param selectors - The selectors function
@@ -54,20 +54,7 @@ export function buildSelectors(__machine, selectors) {
54
54
  * @returns The actions function - ready to be passed to {@link buildView}
55
55
  * */
56
56
  export function buildActions(__machine, __selectors, actions) {
57
- let lastSelectorResult = undefined;
58
- let lastCachedResult = undefined;
59
- let lastSendReference = undefined;
60
- return (send, selectors) => {
61
- if (lastSelectorResult === selectors &&
62
- lastCachedResult !== undefined &&
63
- lastSendReference === send) {
64
- return lastCachedResult;
65
- }
66
- lastCachedResult = actions(send, selectors);
67
- lastSelectorResult = selectors;
68
- lastSendReference = send;
69
- return lastCachedResult;
70
- };
57
+ return actions;
71
58
  }
72
59
  /**
73
60
  * @public
@@ -71,8 +71,8 @@ export declare function broadcast(event: GlobalEvents): void;
71
71
  * - `send` - the interpreters send function, which can be used to send events to the machine
72
72
  * - `selectors` - the output of the selectors function from {@link buildSelectors}
73
73
  *
74
- * The resulting action function has memoization. It will return the same value until the
75
- * selectors reference changes or the send reference changes
74
+ * The resulting action function will only be called once per invocation of a machine.
75
+ * The selectors are passed in as a proxy to always read the latest selector value
76
76
  *
77
77
  * @param machine - The machine to create the actions for
78
78
  * @param selectors - The selectors function
package/lib/xstateTree.js CHANGED
@@ -107,6 +107,7 @@ export function XstateTreeView({ interpreter }) {
107
107
  const [current] = useService(interpreter);
108
108
  const currentRef = useRef(current);
109
109
  currentRef.current = current;
110
+ const selectorsRef = useRef(undefined);
110
111
  const { view: View, actions: actionsFactory, selectors: selectorsFactory, slots: interpreterSlots, } = interpreter.machine.meta;
111
112
  const slots = useSlots(interpreter, interpreterSlots.map((x) => x.name));
112
113
  const canHandleEvent = useCallback((e) => {
@@ -121,11 +122,22 @@ export function XstateTreeView({ interpreter }) {
121
122
  // current state the machine is in changes. But _only_ then
122
123
  // eslint-disable-next-line react-hooks/exhaustive-deps
123
124
  [current.value]);
125
+ const selectorsProxy = useConstant(() => {
126
+ return new Proxy({}, {
127
+ get: (_target, prop) => {
128
+ var _a;
129
+ return (_a = selectorsRef.current) === null || _a === void 0 ? void 0 : _a[prop];
130
+ },
131
+ });
132
+ });
133
+ const actions = useConstant(() => {
134
+ return actionsFactory(interpreter.send, selectorsProxy);
135
+ });
124
136
  if (!current) {
125
137
  return null;
126
138
  }
127
139
  const selectors = selectorsFactory(current.context, canHandleEvent, inState, current.value);
128
- const actions = actionsFactory(interpreter.send, selectors);
140
+ selectorsRef.current = selectors;
129
141
  return (React.createElement(View, { selectors: selectors, actions: actions, slots: slots, inState: inState }));
130
142
  }
131
143
  /**
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@koordinates/xstate-tree",
3
3
  "main": "lib/index.js",
4
4
  "types": "lib/xstate-tree.d.ts",
5
- "version": "2.0.9",
5
+ "version": "2.0.11",
6
6
  "license": "MIT",
7
7
  "description": "Build UIs with Actors using xstate and React",
8
8
  "keywords": [
@@ -41,6 +41,8 @@
41
41
  "@types/react-dom": "^18.0.6",
42
42
  "@types/testing-library__jest-dom": "^5.14.1",
43
43
  "@typescript-eslint/eslint-plugin": "^5.30.5",
44
+ "@vitejs/plugin-react": "^2.1.0",
45
+ "@xstate/immer": "^0.3.1",
44
46
  "@xstate/react": "^3.0.0",
45
47
  "classnames": "^2.3.1",
46
48
  "cz-conventional-changelog": "^3.3.0",
@@ -51,16 +53,22 @@
51
53
  "eslint-plugin-react-hooks": "^4.3.0",
52
54
  "history": "^4.10.1",
53
55
  "husky": "^8.0.1",
56
+ "immer": "^9.0.15",
54
57
  "jest": "^28.0.3",
55
58
  "jest-environment-jsdom": "^28.0.1",
56
59
  "react": "^18.1.0",
57
60
  "react-dom": "^18.1.0",
58
61
  "rimraf": "^3.0.2",
62
+ "rxjs": "^7.5.6",
59
63
  "semantic-release": "^19.0.3",
60
64
  "semantic-release-npm-github-publish": "^1.5.1",
65
+ "todomvc-app-css": "^2.4.2",
66
+ "todomvc-common": "^1.0.5",
61
67
  "ts-jest": "^28.0.5",
62
68
  "typescript": "^4.7.3",
63
- "xstate": "^4.32.0"
69
+ "vite": "^3.1.3",
70
+ "vite-tsconfig-paths": "^3.5.0",
71
+ "xstate": "^4.33.0"
64
72
  },
65
73
  "peerDependencies": {
66
74
  "@xstate/react": "^3.x",
@@ -70,7 +78,9 @@
70
78
  "scripts": {
71
79
  "lint": "eslint 'src/**/*'",
72
80
  "test": "jest",
73
- "build": "rimraf dist && tsc -p tsconfig.build.json",
81
+ "test-examples": "tsc --noEmit",
82
+ "todomvc": "vite dev",
83
+ "build": "rimraf lib && rimraf out && tsc -p tsconfig.build.json",
74
84
  "build:watch": "tsc -p tsconfig.build.json -w",
75
85
  "api-extractor": "api-extractor run",
76
86
  "release": "semantic-release",