Skip to content

Commit 09d756b

Browse files
refactor(proxy-state-tree): refactor naming and write initial docs
1 parent 2d8a845 commit 09d756b

File tree

5 files changed

+130
-162
lines changed

5 files changed

+130
-162
lines changed
Lines changed: 107 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# proxy-state-tree
2-
An implementation of the Mobx/Vue state tracking approach, for library authors
2+
An implementation of the Mobx/Vue state tracking approach with a state tree, for library authors
33

44
`npm install proxy-state-tree`
55

@@ -10,205 +10,178 @@ The **proxy-state-tree** project is created to stimulate innovation in state man
1010

1111
**proxy-state-tree** is a low level implementation of the **getter/setter interception** with a **single state tree** to help library authors innovate. I hope to see innovations that removes the burden that immutability currently causes, but keeps the guarantees that was introduced in **Flux**. I invite you to make a mobx and redux baby! ;-)
1212

13-
## Examples
14-
15-
- [Vue](https://codesandbox.io/s/5vy5jxrpop) example with a simple implementation that allows you to define a state tree and expose a store to the components which can be mutated in the methods
16-
17-
- [Preact](https://codesandbox.io/s/lpmv68r8y9) example with a simple implementation of a external state and actions passed on a Provider. Also includes a simple, but inspiring debugger
18-
1913
## Create a tree
2014

2115
```js
22-
import ProxyStateTree from 'proxy-state-tree'
16+
import { ProxyStateTree } from 'proxy-state-tree'
2317

24-
const tree = new ProxyStateTree({})
18+
const initialState = {}
2519

26-
console.log(tree.get()) // {}
20+
const tree = new ProxyStateTree(initialState)
2721
```
2822

29-
As a library author you would typically expose a mechanism to define the initial state of the application, which you would pass to the **ProxyStateTree**. You would also expose a way to access the state, hiding the `tree.get()` from the consumer of your library.
23+
As a library author you would typically expose a mechanism to define the initial state of the application, which you would pass to the **ProxyStateTree**.
3024

31-
## Track access
32-
33-
You can track access to the state by using the **startPathsTracking** and **clearPathsTracking** methods.
25+
## TrackStateTree
3426

3527
```js
36-
import ProxyStateTree from 'proxy-state-tree'
37-
3828
const tree = new ProxyStateTree({
39-
foo: 'bar',
40-
bar: 'baz'
29+
foo: 'bar'
4130
})
42-
const state = tree.get()
4331

44-
const trackId = tree.startPathsTracking()
45-
const foo = state.foo
46-
const bar = state.bar
47-
const paths = tree.clearPathsTracking(trackId)
32+
const trackStateTree = tree.getTrackStateTree()
4833

49-
console.log(paths) // Set { 'foo', 'bar' }
34+
trackStateTree.state.foo // "bar"
5035
```
5136

52-
You would typically use this mechanism to track usage of state. For example rendering a component, calculating a a computed value etc. The returned paths set is stored for later usage. The paths structure is used internally by proxy-state-tree, but you can also consume it as a library author to for example showing components and what paths they depend on in a devtool. Nested paths uses dot notation, for example `['foo.bar']`. Path tracking needs to be synchronous from the beginning of the tracking, but the clearing of tracking can be asynchronous.
37+
This is a "fork" of the tree which allows you to access and track access to state. You can have multiple forks and you would typically give each component its own state tracking tree.
5338

54-
## Track mutations
39+
### track
5540

5641
```js
57-
import ProxyStateTree from 'proxy-state-tree'
42+
const trackStateTree = tree.getTrackStateTree()
5843

59-
const tree = new ProxyStateTree({
60-
foo: 'bar',
61-
bar: []
44+
trackStateTree.track(() => {
45+
// Called when any tracked state mutates
6246
})
63-
const state = tree.get()
64-
65-
tree.startMutationTracking()
66-
state.foo = 'bar2'
67-
state.bar.push('baz')
68-
const mutations = tree.clearMutationTracking()
69-
70-
console.log(mutations)
71-
/*
72-
[{
73-
method: 'set',
74-
path: ['foo'],
75-
args: ['bar2']
76-
}, {
77-
method: 'push',
78-
path: ['bar'],
79-
args: ['baz']
80-
}]
81-
*/
8247
```
8348

84-
You would use **startMutationTracking** and **clearMutationTracking** around logic that is allowed to do mutations, for example actions or similar. Trying to mutate without this tracking active results in an error. The returned array can be used in combination with a devtool.
49+
You only need to start tracking state, there is no need to stop tracking. The reason is that only one forked tree can track at any time. That means one tree stops tracking when an other starts tracking. Since all component libraries produces their UI description synchronously, this gives a predictable behaviour. It is also makes the implementation simple. The tracking of each fork stops on the next event loop.
8550

86-
## Check need to update
51+
## trackScope
8752

8853
```js
89-
import ProxyStateTree from 'proxy-state-tree'
54+
const trackStateTree = tree.getTrackStateTree()
9055

91-
const tree = new ProxyStateTree({
92-
foo: 'bar',
93-
bar: 'baz'
56+
trackStateTree.trackScope((tree) => {
57+
// Some logic accessing the state of the tree
9458
})
95-
const state = tree.get()
59+
```
9660

97-
function render () {
98-
const trackId = tree.startPathsTracking()
99-
const foo = state.foo
100-
const bar = state.bar
61+
Sometimes you do want to scope the tracking to a callback. This ensures that the tracking indeed runs completely synchronous within the scope of the callback. Optionally you can also give a callback to be notified when mutations affects the tracked state.
10162

102-
return tree.clearPathsTracking(trackId)
103-
}
63+
```js
64+
const trackStateTree = tree.getTrackStateTree()
10465

105-
const listener = tree.addFlushListener(render(), (flushId) => {
106-
// Runs when mutations matches paths passed in
66+
trackStateTree.trackScope((tree) => {
67+
// Some logic accessing the state of the tree
68+
}, () => {
69+
// Notifies about changes
70+
})
71+
```
72+
73+
### addTrackingPath
10774

108-
// Whenever mutations affecting these paths occurs
109-
// we typically create the paths again due to possible
110-
// conditional logic, in "render" in this example
111-
listener.update(render())
75+
```js
76+
const trackStateTree = tree.getTrackStateTree()
77+
78+
trackStateTree.addTrackingPath('foo.bar')
79+
```
80+
81+
You can manually add paths that the tree should track.
82+
83+
## MutationTree
84+
85+
```js
86+
const tree = new ProxyStateTree({
87+
foo: 'bar'
11288
})
11389

114-
tree.startMutationTracking()
115-
state.foo = 'bar2'
116-
state.bar.push('baz')
117-
tree.clearMutationTracking()
90+
const mutationTree = tree.getMutationTree()
11891

119-
// This command flushes out the current mutations and
120-
// notifies any listeners
121-
tree.flush()
92+
mutationTree.state.foo = "bar"
93+
```
94+
95+
This forked tree is allowed to perform actual mutations.
96+
97+
### onMutation
98+
99+
```js
100+
const mutationTree = tree.getMutationTree()
122101

123-
// Remove listener
124-
listener.dispose()
102+
mutationTree.onMutation((mutation) => {
103+
// mutation: { method, args, path }
104+
})
125105
```
126106

127-
Here we combine the tracked paths with the mutations performed to see if this components, computed or whatever indeed needs to run again, doing a new **startPathsTracking** and **clearPathsTracking**.
107+
Allows you to listen to mutations on the specific tree.
128108

129-
You can optionally declare a global listener which will be informed by all mutation flushes:
109+
## diposeTree
130110

131111
```js
132-
import ProxyStateTree from 'proxy-state-tree'
112+
const tree = new ProxyStateTree({})
113+
114+
const trackStateTree = tree.getTrackStateTree()
115+
116+
tree.disposeTree(trackStateTree)
117+
```
118+
119+
Allows you to dipose of a tree no longer in use. ProxyStateTree will keep a reference and reuse the tree whenever a new fork is requested.
120+
121+
## flush
133122

123+
```js
134124
const tree = new ProxyStateTree({
135-
foo: 'bar',
136-
bar: 'baz'
137-
})
138-
const state = tree.get()
139-
140-
const listener = tree.addFlushListener((mutations, flushId) => {
141-
/*
142-
[{
143-
method: "set",
144-
path: "foo",
145-
args: ["bar2"]
146-
}]
147-
*/
125+
foo: 'bar'
148126
})
149127

150-
tree.startMutationTracking()
151-
state.foo = 'bar2'
152-
tree.clearMutationTracking()
128+
const mutationTree = tree.getMutationTree()
129+
130+
mutationTree.state.foo = "bar"
131+
153132
tree.flush()
154-
listener.dispose()
155133
```
156134

157-
In addition to this you can also listen to each individual mutation:
135+
To notify trees tracking state about mutations made run the **flush** method. This allows you to control when the trackers should actually be notified.
158136

159-
```js
160-
import ProxyStateTree from 'proxy-state-tree'
137+
## onMutation
161138

139+
```js
162140
const tree = new ProxyStateTree({
163-
foo: 'bar',
164-
bar: 'baz'
165-
})
166-
const state = tree.get()
167-
168-
const listener = tree.addMutationListener((mutation, paths, flushId) => {
169-
/*
170-
mutation: {
171-
method: "set",
172-
path: "foo",
173-
args: ["bar2"]
174-
}
175-
paths: Set(["foo"])
176-
flushId: 1
177-
*/
141+
foo: 'bar'
178142
})
179143

180-
tree.startMutationTracking()
181-
state.foo = 'bar2'
182-
tree.clearMutationTracking()
183-
listener.dispose()
144+
tree.onMutation(() => {})
184145
```
185146

186-
It is important here to check the **paths** argument for a list of all paths possibly changed. Reason is that a single mutation might cause changes to multiple paths.
187-
188-
## Dynamic state values
147+
Get notified when any mutation is made to any fork of **MutationTree**.
189148

190-
If you insert a function into the state tree it will be called when accessed. The function is passed the **proxy-state-tree** instance and the path of where the function lives in the tree.
149+
## onFlush
191150

192151
```js
193-
import ProxyStateTree from 'proxy-state-tree'
194-
195152
const tree = new ProxyStateTree({
196-
foo: (proxyStateTree, path) => {}
153+
foo: 'bar'
197154
})
155+
156+
tree.onFlush(() => {})
198157
```
199158

200-
The allows you to easily extend functionality with for example a computed concept that lives in the tree, as you can see in this [codesandbox](https://codesandbox.io/s/xnv45zmkz).
159+
Get notified when a flush is made.
201160

202-
You can inject a wrapper around this function by:
203161

204-
```js
205-
import ProxyStateTree from 'proxy-state-tree'
162+
## rescope
206163

164+
```js
207165
const tree = new ProxyStateTree({
208-
foo: (foo, proxyStateTree, path) => {}
209-
}, {
210-
dynamicWrapper: (proxyStateTree, path, func) => func('foo', proxyStateTree, path)
166+
foo: 'bar'
211167
})
168+
169+
const trackStateTree = tree.getTrackStateTree()
170+
const mutationTree = tree.getMutationTree()
171+
172+
// This is now a proxy tracked by TrackStateTrees
173+
const foo = trackStateTree.state.foo
174+
175+
// We can rescope this value to a MutationTree
176+
tree.rescope(foo, mutationTree)
212177
```
213178

214-
This helps you expose library entities to these functions.
179+
Rescoping proxies between trees is useful in development as the MutationTrees has their own proxies. Unlike in production where all trees shares the same proxies.
180+
181+
## Production
182+
183+
When running in development all **TrackStateTree** forks has the same *proxifier*, meaning they share proxies. They can do this cause the trees "hand over" tracking to each other.
184+
185+
Every fork of a **MutationTree** has its own *proxifier*. The reason for this is that in development each mutation tree fork should live on its own for tracking purposes.
186+
187+
When running in production there is only one *proxifier* shared among all trees, and there is only one mutationtree instance as tracking is no longer needed.

packages/node_modules/proxy-state-tree/src/TrackMutationTree.ts renamed to packages/node_modules/proxy-state-tree/src/MutationTree.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import {
22
IProxyStateTree,
3-
ITrackMutationTree,
3+
IMutationTree,
44
IMutationCallback,
55
IMutation,
66
IProxifier,
77
} from './types'
88
import { Proxifier } from './Proxyfier'
99

10-
export class TrackMutationTree<T extends object>
11-
implements ITrackMutationTree<T> {
10+
export class MutationTree<T extends object> implements IMutationTree<T> {
1211
private mutationCallbacks: IMutationCallback[] = []
1312
master: IProxyStateTree<T>
1413
state: T

packages/node_modules/proxy-state-tree/src/Proxyfier.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TTree, ITrackStateTree, ITrackMutationTree, IMutation } from './types'
1+
import { TTree, ITrackStateTree, IMutationTree } from './types'
22
import isPlainObject from 'is-plain-obj'
33

44
export const IS_PROXY = Symbol('IS_PROXY')
@@ -150,9 +150,7 @@ export class Proxifier {
150150
return null
151151
}
152152
getMutationTree() {
153-
return (
154-
this.tree.master.mutationTree || (this.tree as ITrackMutationTree<any>)
155-
)
153+
return this.tree.master.mutationTree || (this.tree as IMutationTree<any>)
156154
}
157155
private createArrayProxy(value, path) {
158156
var proxifier = this

0 commit comments

Comments
 (0)