|
| 1 | +--- |
| 2 | +"fluid-framework": minor |
| 3 | +"@fluidframework/tree": minor |
| 4 | +"@fluid-experimental/tree-react-api": minor |
| 5 | +"@fluidframework/react": minor |
| 6 | +"__section": tree |
| 7 | +--- |
| 8 | +Added APIs for tracking observations of SharedTree content for automatic invalidation |
| 9 | + |
| 10 | +`TreeAlpha.trackObservations` and `TreeAlpha.trackObservationsOnce` have been added. |
| 11 | +These provide a way to run some operation which reads content from [TreeNodes](https://fluidframework.com/docs/api/tree/treenode-class), then run a call back when anything observed by that operation changes. |
| 12 | + |
| 13 | +This functionality has also been exposed in the form of React hooks and React higher order components via the `@fluid-experimental/tree-react-api` package. |
| 14 | +It is now possible to use these utilities to implement React applications which pass TreeNodes in their props and get all necessary invalidation from tree changes handled automatically. |
| 15 | +The recommended pattern for doing this is to use `treeDataObject` or `TreeViewComponent` at the root, then `withTreeObservations` or `withMemoizedTreeObservations` for any sub-components which read from TreeNodes. |
| 16 | +Alternatively more localized changes can be made by using `PropNode` to type erase TreeNodes passed in props, then use one of the `usePropTreeNode` or `usePropTreeRecord` hooks to read from them. |
| 17 | + |
| 18 | +These APIs work with both hydrated and [un-hydrated](https://fluidframework.com/docs/api/tree/unhydrated-typealias) TreeNodes. |
| 19 | + |
| 20 | +### React Support |
| 21 | + |
| 22 | +Here is a simple example of a React components which has an invalidation bug due to reading a mutable field from a TreeNode that was provided in a prop: |
| 23 | + |
| 24 | +```typescript |
| 25 | +const builder = new SchemaFactory("example"); |
| 26 | +class Item extends builder.object("Item", { text: SchemaFactory.string }) {} |
| 27 | +const ItemComponentBug = ({ item }: { item: Item }): JSX.Element => ( |
| 28 | + <span>{item.text}</span> // Reading `text`, a mutable value from a React prop, causes an invalidation bug. |
| 29 | +); |
| 30 | +``` |
| 31 | + |
| 32 | +This bug can now easily be fixed using `withTreeObservations` or ``withMemoizedTreeObservations`: |
| 33 | + |
| 34 | +```typescript |
| 35 | +const ItemComponent = withTreeObservations( |
| 36 | + ({ item }: { item: Item }): JSX.Element => <span>{item.text}</span>, |
| 37 | +); |
| 38 | +``` |
| 39 | + |
| 40 | +For components which take in TreeNodes, but merely forward them and do not read their properties, they can use `PropTreeNode` as shown: |
| 41 | + |
| 42 | +```typescript |
| 43 | +const ItemParentComponent = ({ item }: { item: PropTreeNode<Item> }): JSX.Element => ( |
| 44 | + <ItemComponent item={item} /> |
| 45 | +); |
| 46 | +``` |
| 47 | + |
| 48 | +If such a component reads from the TreeNode, it gets a compile error instead of an invalidation bug. |
| 49 | +In this case the invalidation bug would be that if `item.text` is modified, the component would not re-render. |
| 50 | + |
| 51 | +```typescript |
| 52 | +const InvalidItemParentComponent = ({ |
| 53 | + item, |
| 54 | +}: { item: PropTreeNode<Item> }): JSX.Element => ( |
| 55 | + // @ts-expect-error PropTreeNode turns this invalidation bug into a compile error |
| 56 | + <span>{item.text}</span> |
| 57 | +); |
| 58 | +``` |
| 59 | + |
| 60 | +To provide access to TreeNode content in only part of a component the `usePropTreeNode` or `usePropTreeRecord` hooks can be used. |
| 61 | + |
| 62 | + |
| 63 | +### TreeAlpha.trackObservationsOnce Examples |
| 64 | + |
| 65 | +Here is a rather minimal example of how `TreeAlpha.trackObservationsOnce` can be used: |
| 66 | + |
| 67 | +```typescript |
| 68 | +cachedFoo ??= TreeAlpha.trackObservationsOnce( |
| 69 | + () => { |
| 70 | + cachedFoo = undefined; |
| 71 | + }, |
| 72 | + () => nodeA.someChild.bar + nodeB.someChild.baz, |
| 73 | +).result; |
| 74 | +``` |
| 75 | + |
| 76 | +That is equivalent to doing the following: |
| 77 | + |
| 78 | +```typescript |
| 79 | +if (cachedFoo === undefined) { |
| 80 | + cachedFoo = nodeA.someChild.bar + nodeB.someChild.baz; |
| 81 | + const invalidate = (): void => { |
| 82 | + cachedFoo = undefined; |
| 83 | + for (const u of unsubscribe) { |
| 84 | + u(); |
| 85 | + } |
| 86 | + }; |
| 87 | + const unsubscribe: (() => void)[] = [ |
| 88 | + TreeBeta.on(nodeA, "nodeChanged", (data) => { |
| 89 | + if (data.changedProperties.has("someChild")) { |
| 90 | + invalidate(); |
| 91 | + } |
| 92 | + }), |
| 93 | + TreeBeta.on(nodeB, "nodeChanged", (data) => { |
| 94 | + if (data.changedProperties.has("someChild")) { |
| 95 | + invalidate(); |
| 96 | + } |
| 97 | + }), |
| 98 | + TreeBeta.on(nodeA.someChild, "nodeChanged", (data) => { |
| 99 | + if (data.changedProperties.has("bar")) { |
| 100 | + invalidate(); |
| 101 | + } |
| 102 | + }), |
| 103 | + TreeBeta.on(nodeB.someChild, "nodeChanged", (data) => { |
| 104 | + if (data.changedProperties.has("baz")) { |
| 105 | + invalidate(); |
| 106 | + } |
| 107 | + }), |
| 108 | + ]; |
| 109 | +} |
| 110 | +``` |
| 111 | + |
| 112 | +Here is more complete example showing how to use `TreeAlpha.trackObservationsOnce` invalidate a property derived from its tree fields. |
| 113 | + |
| 114 | +```typescript |
| 115 | +const factory = new SchemaFactory("com.example"); |
| 116 | +class Vector extends factory.object("Vector", { |
| 117 | + x: SchemaFactory.number, |
| 118 | + y: SchemaFactory.number, |
| 119 | +}) { |
| 120 | + #length: number | undefined = undefined; |
| 121 | + public length(): number { |
| 122 | + if (this.#length === undefined) { |
| 123 | + const result = TreeAlpha.trackObservationsOnce( |
| 124 | + () => { |
| 125 | + this.#length = undefined; |
| 126 | + }, |
| 127 | + () => Math.hypot(this.x, this.y), |
| 128 | + ); |
| 129 | + this.#length = result.result; |
| 130 | + } |
| 131 | + return this.#length; |
| 132 | + } |
| 133 | +} |
| 134 | +const vec = new Vector({ x: 3, y: 4 }); |
| 135 | +assert.equal(vec.length(), 5); |
| 136 | +vec.x = 0; |
| 137 | +assert.equal(vec.length(), 4); |
| 138 | +``` |
0 commit comments