@tomorrowevening/theatre-react 1.0.4 → 1.0.8

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
@@ -1,4 +1,4 @@
1
- # @theatre/react
1
+ # @tomorrowevening/theatre-react
2
2
 
3
3
  Utilities for using [Theatre.js](https://www.theatrejs.com) or
4
4
  [Dataverse](https://github.com/theatre-js/theatre/tree/main/packages/dataverse)
@@ -13,8 +13,8 @@ A React hook that returns the value of the given prism or pointer.
13
13
  Usage with Dataverse pointers:
14
14
 
15
15
  ```tsx
16
- import {Atom} from '@theatre/dataverse'
17
- import {useVal} from '@theatre/react'
16
+ import {Atom} from '@tomorrowevening/theatre-dataverse'
17
+ import {useVal} from '@tomorrowevening/theatre-react'
18
18
 
19
19
  const atom = new Atom({foo: 'foo'})
20
20
 
@@ -27,8 +27,8 @@ function Component() {
27
27
  Usage with Dataverse prisms:
28
28
 
29
29
  ```tsx
30
- import {prism} from '@theatre/dataverse'
31
- import {useVal} from '@theatre/react'
30
+ import {prism} from '@tomorrowevening/theatre-dataverse'
31
+ import {useVal} from '@tomorrowevening/theatre-react'
32
32
 
33
33
  const pr = prism(() => 'some value')
34
34
 
@@ -41,8 +41,8 @@ function Component() {
41
41
  Usage with Theatre.js pointers:
42
42
 
43
43
  ```tsx
44
- import {useVal} from '@theatre/react'
45
- import {getProject} from '@theatre/core'
44
+ import {useVal} from '@tomorrowevening/theatre-react'
45
+ import {getProject} from '@tomorrowevening/theatre-core'
46
46
 
47
47
  const obj = getProject('my project')
48
48
  .sheet('my sheet')
@@ -65,8 +65,8 @@ Creates a prism out of `fn` and subscribes the element to the value of the
65
65
  created prism.
66
66
 
67
67
  ```tsx
68
- import {Atom, val, prism} from '@theatre/dataverse'
69
- import {usePrism} from '@theatre/react'
68
+ import {Atom, val, prism} from '@tomorrowevening/theatre-dataverse'
69
+ import {usePrism} from '@tomorrowevening/theatre-react'
70
70
 
71
71
  const state = new Atom({a: 1, b: 1})
72
72
 
@@ -94,8 +94,8 @@ function Component(props: {which: 'a' | 'b'}) {
94
94
  Subscribes the element to the value of the given prism instance.
95
95
 
96
96
  ```tsx
97
- import {Atom, val, prism} from '@theatre/dataverse'
98
- import {usePrismInstance} from '@theatre/react'
97
+ import {Atom, val, prism} from '@tomorrowevening/theatre-dataverse'
98
+ import {usePrismInstance} from '@tomorrowevening/theatre-react'
99
99
 
100
100
  const state = new Atom({a: 1, b: 1})
101
101
 
@@ -115,7 +115,7 @@ function Component() {
115
115
  re-render if the value of the atom changes.
116
116
 
117
117
  ```tsx
118
- import {useAtom, useVal} from '@theatre/react'
118
+ import {useAtom, useVal} from '@tomorrowevening/theatre-react'
119
119
  import {useEffect} from 'react'
120
120
 
121
121
  function MyComponent() {
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
- "sourcesContent": ["/**\r\n * React bindings for dataverse.\r\n *\r\n * @packageDocumentation\r\n */\r\n\r\nimport type {Prism} from '@tomorrowevening/theatre-dataverse'\r\nimport {prism, val, Atom} from '@tomorrowevening/theatre-dataverse'\r\nimport {findIndex} from 'lodash-es'\r\nimport queueMicrotask from 'queue-microtask'\r\nimport {useCallback, useLayoutEffect, useRef, useState} from 'react'\r\nimport {unstable_batchedUpdates} from 'react-dom'\r\n\r\ntype $IntentionalAny = any\r\ntype VoidFn = () => void\r\n\r\n/**\r\n * Enables a few traces and debug points to help identify performance glitches in `@theatre/react`.\r\n * Look up references to this value to see how to make use of those traces.\r\n */\r\nconst TRACE: boolean = false && process.env.NODE_ENV !== 'production'\r\n\r\nfunction useForceUpdate(debugLabel?: string) {\r\n const [, setTick] = useState(0)\r\n\r\n const update = useCallback(() => {\r\n setTick((tick) => tick + 1)\r\n }, [])\r\n\r\n return update\r\n}\r\n\r\n/**\r\n * A React hook that executes the callback function and returns its return value\r\n * whenever there's a change in the values of the dependency array, or in the\r\n * prisms that are used within the callback function.\r\n *\r\n * @param fn - The callback function\r\n * @param deps - The dependency array\r\n * @param debugLabel - The label used by the debugger\r\n *\r\n * @remarks\r\n *\r\n * A common mistake with `usePrism()` is not including its deps in its dependency array. Let's\r\n * have an eslint rule to catch that.\r\n */\r\nexport function usePrism<T>(\r\n fn: () => T,\r\n deps: unknown[],\r\n debugLabel?: string,\r\n): T {\r\n const fnAsCallback = useCallback(fn, deps)\r\n const atomRef = useRef<Atom<typeof fn>>(null as $IntentionalAny)\r\n if (!atomRef.current) {\r\n atomRef.current = new Atom(fnAsCallback)\r\n } else {\r\n atomRef.current.set(fnAsCallback)\r\n }\r\n\r\n const prismRef = useRef<Prism<T> | null>(null)\r\n\r\n if (!prismRef.current) {\r\n prismRef.current = prism(() => {\r\n const fn = atomRef.current.prism.getValue()\r\n return fn()\r\n })\r\n }\r\n\r\n return usePrismInstance(prismRef.current, debugLabel)\r\n}\r\n\r\nexport const useVal: typeof val = (p: $IntentionalAny, debugLabel?: string) => {\r\n return usePrism(() => val(p), [p], debugLabel)\r\n}\r\n\r\n/**\r\n * Each usePrism() call is assigned an `order`. Parents have a smaller\r\n * order than their children, and so on.\r\n */\r\nlet lastOrder = 0\r\n\r\n/**\r\n * A sorted array of prisms that need to be refreshed. The prisms are sorted\r\n * by their order, which means a parent prism always gets priority to children\r\n * and descendents. Ie. we refresh the prisms top to bottom.\r\n */\r\nconst queue: QueueItem[] = []\r\nconst setOfQueuedItems = new Set<QueueItem>()\r\n\r\ntype QueueItem<T = unknown> = {\r\n order: number\r\n /**\r\n * runUpdate() is the equivalent of a forceUpdate() call. It would only be called\r\n * if the value of the inner prism has _actually_ changed.\r\n */\r\n runUpdate: VoidFn\r\n /**\r\n * Some debugging info that are only present if {@link TRACE} is true\r\n */\r\n debug?: {\r\n /**\r\n * The `debugLabel` given to `usePrism()/usePrismInstance()`\r\n */\r\n label?: string\r\n /**\r\n * A trace of the first time the component got rendered\r\n */\r\n traceOfFirstTimeRender: Error\r\n /**\r\n * An array of the operations done on/about this usePrismInstance. This is helpful to trace\r\n * why a usePrismInstance's update was added to the queue and why it re-rendered\r\n */\r\n history: Array<\r\n /**\r\n * Item reached its turn in the queue\r\n */\r\n | `queue reached`\r\n /**\r\n * Item reached its turn in the queue, and errored (likely something in `prism()` threw an error)\r\n */\r\n | `queue: der.getValue() errored`\r\n /**\r\n * The item was added to the queue (may be called multiple times, but will only queue once)\r\n */\r\n | `queueUpdate()`\r\n /**\r\n * `cb` in `item.der.onStale(cb)` was called\r\n */\r\n | `onStale(cb)`\r\n /**\r\n * Item was rendered\r\n */\r\n | `rendered`\r\n >\r\n }\r\n /**\r\n * A reference to the prism\r\n */\r\n der: Prism<T>\r\n /**\r\n * The last value of this prism.\r\n */\r\n lastValue: T\r\n /**\r\n * Would be set to true if the element hosting the `usePrismInstance()` was unmounted\r\n */\r\n unmounted: boolean\r\n /**\r\n * Adds the `usePrismInstance` to the update queue\r\n */\r\n queueUpdate: () => void\r\n /**\r\n * Untaps from `this.der.unStale()`\r\n */\r\n untap: () => void\r\n}\r\n\r\nlet microtaskIsQueued = false\r\n\r\nconst pushToQueue = (item: QueueItem) => {\r\n _pushToQueue(item)\r\n queueIfNeeded()\r\n}\r\n\r\nconst _pushToQueue = (item: QueueItem) => {\r\n if (setOfQueuedItems.has(item)) return\r\n setOfQueuedItems.add(item)\r\n\r\n if (queue.length === 0) {\r\n queue.push(item)\r\n } else {\r\n const index = findIndex(\r\n queue,\r\n (existingItem) => existingItem.order >= item.order,\r\n )\r\n if (index === -1) {\r\n queue.push(item)\r\n } else {\r\n const right = queue[index]\r\n if (right.order > item.order) {\r\n queue.splice(index, 0, item)\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Plucks items from the queue\r\n */\r\nconst removeFromQueue = (item: QueueItem) => {\r\n if (!setOfQueuedItems.has(item)) return\r\n setOfQueuedItems.delete(item)\r\n\r\n const index = findIndex(queue, (o) => o === item)\r\n queue.splice(index, 1)\r\n}\r\n\r\nfunction queueIfNeeded() {\r\n if (microtaskIsQueued) return\r\n microtaskIsQueued = true\r\n\r\n queueMicrotask(() => {\r\n unstable_batchedUpdates(function runQueue() {\r\n while (queue.length > 0) {\r\n const item = queue.shift()!\r\n setOfQueuedItems.delete(item)\r\n\r\n let newValue\r\n if (TRACE) {\r\n item.debug?.history.push(`queue reached`)\r\n }\r\n try {\r\n newValue = item.der.getValue()\r\n } catch (error) {\r\n if (TRACE) {\r\n item.debug?.history.push(`queue: der.getValue() errored`)\r\n }\r\n console.error(\r\n 'A `der.getValue()` in `usePrismInstance(der)` threw an error. ' +\r\n \"This may be a zombie child issue, so we're gonna try to get its value again in a normal react render phase.\" +\r\n 'If you see the same error again, then you either have an error in your prism code, or the deps array in `usePrism(fn, deps)` is missing ' +\r\n 'a dependency and causing the prism to read stale values.',\r\n )\r\n console.error(error)\r\n\r\n item.runUpdate()\r\n\r\n continue\r\n }\r\n if (newValue !== item.lastValue) {\r\n item.lastValue = newValue\r\n item.runUpdate()\r\n }\r\n }\r\n\r\n microtaskIsQueued = false\r\n }, 1)\r\n })\r\n}\r\n/**\r\n * A React hook that returns the value of the prism that it received as the first argument.\r\n * It works like an implementation of Dataverse's Ticker, except that it runs the side effects in\r\n * an order where a component's prism is guaranteed to run before any of its descendents' prisms.\r\n *\r\n * @param der - The prism\r\n * @param debugLabel - The label used by the debugger\r\n *\r\n * @remarks\r\n * It looks like this new implementation of usePrism() manages to:\r\n * 1. Not over-calculate the prisms\r\n * 2. Render prism in ancestor -\\> descendent order\r\n * 3. Not set off React's concurrent mode alarms\r\n *\r\n *\r\n * I'm happy with how little bookkeeping we ended up doing here.\r\n *\r\n * ---\r\n *\r\n * Notes on the latest implementation:\r\n *\r\n * # Remove cold prism reads\r\n *\r\n * Prior to the latest change, the first render of every `usePrismInstance()` resulted in a cold read of its inner prism.\r\n * Cold reads are predictably slow. The reason we'd run cold reads was to comply with react's rule of not running side-effects\r\n * during render. (Turning a prism hot is _technically_ a side-effect).\r\n *\r\n * However, now that users are animating scenes with hundreds of objects in the same sequence, the lag started to be noticable.\r\n *\r\n * This commit changes `usePrismInstance()` so that it turns its prism hot before rendering them.\r\n *\r\n * # Freshen prisms before render\r\n *\r\n * Previously in order to avoid the zombie child problem (https://kaihao.dev/posts/stale-props-and-zombie-children-in-redux)\r\n * we deferred freshening the prisms to the render phase of components. This meant that if a prism's dependencies\r\n * changed, `usePrismInstance()` would schedule a re-render, regardless of whether that change actually affected the prism's\r\n * value. Here is a contrived example:\r\n *\r\n * ```ts\r\n * const num = new Box(1)\r\n * const isPositiveD = prism(() => num.prism.getValue() >= 0)\r\n *\r\n * const Comp = () => {\r\n * return <div>{usePrismInstance(isPositiveD)}</div>\r\n * }\r\n *\r\n * num.set(2) // would cause Comp to re-render- even though 1 is still a positive number\r\n * ```\r\n *\r\n * We now avoid this problem by freshening the prism (i.e. calling `der.getValue()`) inside `runQueue()`,\r\n * and then only causing a re-render if the prism's value is actually changed.\r\n *\r\n * This still avoids the zombie-child problem because `runQueue` reads the prisms in-order of their position in\r\n * the mounting tree.\r\n *\r\n * On the off-chance that one of them still turns out to be a zombile child, `runQueue` will defer that particular\r\n * `usePrismInstance()` to be read inside a normal react render phase.\r\n */\r\nexport function usePrismInstance<T>(der: Prism<T>, debugLabel?: string): T {\r\n const _forceUpdate = useForceUpdate(debugLabel)\r\n\r\n const ref = useRef<QueueItem<T>>(undefined as $IntentionalAny)\r\n\r\n if (!ref.current) {\r\n lastOrder++\r\n\r\n ref.current = {\r\n order: lastOrder,\r\n runUpdate: () => {\r\n if (!ref.current.unmounted) {\r\n _forceUpdate()\r\n }\r\n },\r\n der,\r\n lastValue: undefined as $IntentionalAny,\r\n unmounted: false,\r\n queueUpdate: () => {\r\n if (TRACE) {\r\n ref.current.debug?.history.push(`queueUpdate()`)\r\n }\r\n pushToQueue(ref.current)\r\n },\r\n untap: der.onStale(() => {\r\n if (TRACE) {\r\n ref.current.debug!.history.push(`onStale(cb)`)\r\n }\r\n ref.current!.queueUpdate()\r\n }),\r\n }\r\n\r\n if (TRACE) {\r\n ref.current.debug = {\r\n label: debugLabel,\r\n traceOfFirstTimeRender: new Error(),\r\n history: [],\r\n }\r\n }\r\n }\r\n\r\n if (process.env.NODE_ENV !== 'production') {\r\n if (der !== ref.current.der) {\r\n console.error(\r\n 'Argument `der` in `usePrismInstance(der)` should not change between renders.',\r\n )\r\n }\r\n }\r\n\r\n useLayoutEffect(() => {\r\n return function onUnmount() {\r\n ref.current.unmounted = true\r\n ref.current.untap()\r\n removeFromQueue(ref.current)\r\n }\r\n }, [])\r\n\r\n // if we're queued but are rendering before our turn, remove us from the queue\r\n removeFromQueue(ref.current)\r\n\r\n const newValue = ref.current.der.getValue()\r\n ref.current.lastValue = newValue\r\n\r\n if (TRACE) {\r\n ref.current.debug?.history.push(`rendered`)\r\n }\r\n\r\n return newValue\r\n}\r\n\r\n/**\r\n * Creates a new Atom, similar to useState(), but the component\r\n * won't re-render if the value of the atom changes.\r\n *\r\n * @param initialState - Initial state\r\n * @returns The Atom\r\n *\r\n * @example\r\n *\r\n * Usage\r\n * ```tsx\r\n * import {useAtom, useVal} from '@theatre/react'\r\n * import {useEffect} from 'react'\r\n *\r\n * function MyComponent() {\r\n * const atom = useAtom({count: 0, ready: false})\r\n *\r\n * const onClick = () =>\r\n * atom.setByPointer(\r\n * (p) => p.count,\r\n * (count) => count + 1,\r\n * )\r\n *\r\n * useEffect(() => {\r\n * setTimeout(() => {\r\n * atom.setByPointer((p) => p.ready, true)\r\n * }, 1000)\r\n * }, [])\r\n *\r\n * const ready = useVal(atom.pointer.ready)\r\n * if (!ready) return <div>Loading...</div>\r\n * return <button onClick={onClick}>Click me</button>\r\n * }\r\n * ```\r\n */\r\nexport function useAtom<T>(initialState: T): Atom<T> {\r\n const ref = useRef<Atom<T>>(undefined as $IntentionalAny)\r\n if (!ref.current) {\r\n ref.current = new Atom(initialState)\r\n }\r\n return ref.current\r\n}\r\n"],
4
+ "sourcesContent": ["/**\r\n * React bindings for dataverse.\r\n *\r\n * @packageDocumentation\r\n */\r\n\r\nimport type {Prism} from '@tomorrowevening/theatre-dataverse'\r\nimport {prism, val, Atom} from '@tomorrowevening/theatre-dataverse'\r\nimport {findIndex} from 'lodash-es'\r\nimport queueMicrotask from 'queue-microtask'\r\nimport {useCallback, useLayoutEffect, useRef, useState} from 'react'\r\nimport {unstable_batchedUpdates} from 'react-dom'\r\n\r\ntype $IntentionalAny = any\r\ntype VoidFn = () => void\r\n\r\n/**\r\n * Enables a few traces and debug points to help identify performance glitches in `@tomorrowevening/theatre-react`.\r\n * Look up references to this value to see how to make use of those traces.\r\n */\r\nconst TRACE: boolean = false && process.env.NODE_ENV !== 'production'\r\n\r\nfunction useForceUpdate(debugLabel?: string) {\r\n const [, setTick] = useState(0)\r\n\r\n const update = useCallback(() => {\r\n setTick((tick) => tick + 1)\r\n }, [])\r\n\r\n return update\r\n}\r\n\r\n/**\r\n * A React hook that executes the callback function and returns its return value\r\n * whenever there's a change in the values of the dependency array, or in the\r\n * prisms that are used within the callback function.\r\n *\r\n * @param fn - The callback function\r\n * @param deps - The dependency array\r\n * @param debugLabel - The label used by the debugger\r\n *\r\n * @remarks\r\n *\r\n * A common mistake with `usePrism()` is not including its deps in its dependency array. Let's\r\n * have an eslint rule to catch that.\r\n */\r\nexport function usePrism<T>(\r\n fn: () => T,\r\n deps: unknown[],\r\n debugLabel?: string,\r\n): T {\r\n const fnAsCallback = useCallback(fn, deps)\r\n const atomRef = useRef<Atom<typeof fn>>(null as $IntentionalAny)\r\n if (!atomRef.current) {\r\n atomRef.current = new Atom(fnAsCallback)\r\n } else {\r\n atomRef.current.set(fnAsCallback)\r\n }\r\n\r\n const prismRef = useRef<Prism<T> | null>(null)\r\n\r\n if (!prismRef.current) {\r\n prismRef.current = prism(() => {\r\n const fn = atomRef.current.prism.getValue()\r\n return fn()\r\n })\r\n }\r\n\r\n return usePrismInstance(prismRef.current, debugLabel)\r\n}\r\n\r\nexport const useVal: typeof val = (p: $IntentionalAny, debugLabel?: string) => {\r\n return usePrism(() => val(p), [p], debugLabel)\r\n}\r\n\r\n/**\r\n * Each usePrism() call is assigned an `order`. Parents have a smaller\r\n * order than their children, and so on.\r\n */\r\nlet lastOrder = 0\r\n\r\n/**\r\n * A sorted array of prisms that need to be refreshed. The prisms are sorted\r\n * by their order, which means a parent prism always gets priority to children\r\n * and descendents. Ie. we refresh the prisms top to bottom.\r\n */\r\nconst queue: QueueItem[] = []\r\nconst setOfQueuedItems = new Set<QueueItem>()\r\n\r\ntype QueueItem<T = unknown> = {\r\n order: number\r\n /**\r\n * runUpdate() is the equivalent of a forceUpdate() call. It would only be called\r\n * if the value of the inner prism has _actually_ changed.\r\n */\r\n runUpdate: VoidFn\r\n /**\r\n * Some debugging info that are only present if {@link TRACE} is true\r\n */\r\n debug?: {\r\n /**\r\n * The `debugLabel` given to `usePrism()/usePrismInstance()`\r\n */\r\n label?: string\r\n /**\r\n * A trace of the first time the component got rendered\r\n */\r\n traceOfFirstTimeRender: Error\r\n /**\r\n * An array of the operations done on/about this usePrismInstance. This is helpful to trace\r\n * why a usePrismInstance's update was added to the queue and why it re-rendered\r\n */\r\n history: Array<\r\n /**\r\n * Item reached its turn in the queue\r\n */\r\n | `queue reached`\r\n /**\r\n * Item reached its turn in the queue, and errored (likely something in `prism()` threw an error)\r\n */\r\n | `queue: der.getValue() errored`\r\n /**\r\n * The item was added to the queue (may be called multiple times, but will only queue once)\r\n */\r\n | `queueUpdate()`\r\n /**\r\n * `cb` in `item.der.onStale(cb)` was called\r\n */\r\n | `onStale(cb)`\r\n /**\r\n * Item was rendered\r\n */\r\n | `rendered`\r\n >\r\n }\r\n /**\r\n * A reference to the prism\r\n */\r\n der: Prism<T>\r\n /**\r\n * The last value of this prism.\r\n */\r\n lastValue: T\r\n /**\r\n * Would be set to true if the element hosting the `usePrismInstance()` was unmounted\r\n */\r\n unmounted: boolean\r\n /**\r\n * Adds the `usePrismInstance` to the update queue\r\n */\r\n queueUpdate: () => void\r\n /**\r\n * Untaps from `this.der.unStale()`\r\n */\r\n untap: () => void\r\n}\r\n\r\nlet microtaskIsQueued = false\r\n\r\nconst pushToQueue = (item: QueueItem) => {\r\n _pushToQueue(item)\r\n queueIfNeeded()\r\n}\r\n\r\nconst _pushToQueue = (item: QueueItem) => {\r\n if (setOfQueuedItems.has(item)) return\r\n setOfQueuedItems.add(item)\r\n\r\n if (queue.length === 0) {\r\n queue.push(item)\r\n } else {\r\n const index = findIndex(\r\n queue,\r\n (existingItem) => existingItem.order >= item.order,\r\n )\r\n if (index === -1) {\r\n queue.push(item)\r\n } else {\r\n const right = queue[index]\r\n if (right.order > item.order) {\r\n queue.splice(index, 0, item)\r\n }\r\n }\r\n }\r\n}\r\n\r\n/**\r\n * Plucks items from the queue\r\n */\r\nconst removeFromQueue = (item: QueueItem) => {\r\n if (!setOfQueuedItems.has(item)) return\r\n setOfQueuedItems.delete(item)\r\n\r\n const index = findIndex(queue, (o) => o === item)\r\n queue.splice(index, 1)\r\n}\r\n\r\nfunction queueIfNeeded() {\r\n if (microtaskIsQueued) return\r\n microtaskIsQueued = true\r\n\r\n queueMicrotask(() => {\r\n unstable_batchedUpdates(function runQueue() {\r\n while (queue.length > 0) {\r\n const item = queue.shift()!\r\n setOfQueuedItems.delete(item)\r\n\r\n let newValue\r\n if (TRACE) {\r\n item.debug?.history.push(`queue reached`)\r\n }\r\n try {\r\n newValue = item.der.getValue()\r\n } catch (error) {\r\n if (TRACE) {\r\n item.debug?.history.push(`queue: der.getValue() errored`)\r\n }\r\n console.error(\r\n 'A `der.getValue()` in `usePrismInstance(der)` threw an error. ' +\r\n \"This may be a zombie child issue, so we're gonna try to get its value again in a normal react render phase.\" +\r\n 'If you see the same error again, then you either have an error in your prism code, or the deps array in `usePrism(fn, deps)` is missing ' +\r\n 'a dependency and causing the prism to read stale values.',\r\n )\r\n console.error(error)\r\n\r\n item.runUpdate()\r\n\r\n continue\r\n }\r\n if (newValue !== item.lastValue) {\r\n item.lastValue = newValue\r\n item.runUpdate()\r\n }\r\n }\r\n\r\n microtaskIsQueued = false\r\n }, 1)\r\n })\r\n}\r\n/**\r\n * A React hook that returns the value of the prism that it received as the first argument.\r\n * It works like an implementation of Dataverse's Ticker, except that it runs the side effects in\r\n * an order where a component's prism is guaranteed to run before any of its descendents' prisms.\r\n *\r\n * @param der - The prism\r\n * @param debugLabel - The label used by the debugger\r\n *\r\n * @remarks\r\n * It looks like this new implementation of usePrism() manages to:\r\n * 1. Not over-calculate the prisms\r\n * 2. Render prism in ancestor -\\> descendent order\r\n * 3. Not set off React's concurrent mode alarms\r\n *\r\n *\r\n * I'm happy with how little bookkeeping we ended up doing here.\r\n *\r\n * ---\r\n *\r\n * Notes on the latest implementation:\r\n *\r\n * # Remove cold prism reads\r\n *\r\n * Prior to the latest change, the first render of every `usePrismInstance()` resulted in a cold read of its inner prism.\r\n * Cold reads are predictably slow. The reason we'd run cold reads was to comply with react's rule of not running side-effects\r\n * during render. (Turning a prism hot is _technically_ a side-effect).\r\n *\r\n * However, now that users are animating scenes with hundreds of objects in the same sequence, the lag started to be noticable.\r\n *\r\n * This commit changes `usePrismInstance()` so that it turns its prism hot before rendering them.\r\n *\r\n * # Freshen prisms before render\r\n *\r\n * Previously in order to avoid the zombie child problem (https://kaihao.dev/posts/stale-props-and-zombie-children-in-redux)\r\n * we deferred freshening the prisms to the render phase of components. This meant that if a prism's dependencies\r\n * changed, `usePrismInstance()` would schedule a re-render, regardless of whether that change actually affected the prism's\r\n * value. Here is a contrived example:\r\n *\r\n * ```ts\r\n * const num = new Box(1)\r\n * const isPositiveD = prism(() => num.prism.getValue() >= 0)\r\n *\r\n * const Comp = () => {\r\n * return <div>{usePrismInstance(isPositiveD)}</div>\r\n * }\r\n *\r\n * num.set(2) // would cause Comp to re-render- even though 1 is still a positive number\r\n * ```\r\n *\r\n * We now avoid this problem by freshening the prism (i.e. calling `der.getValue()`) inside `runQueue()`,\r\n * and then only causing a re-render if the prism's value is actually changed.\r\n *\r\n * This still avoids the zombie-child problem because `runQueue` reads the prisms in-order of their position in\r\n * the mounting tree.\r\n *\r\n * On the off-chance that one of them still turns out to be a zombile child, `runQueue` will defer that particular\r\n * `usePrismInstance()` to be read inside a normal react render phase.\r\n */\r\nexport function usePrismInstance<T>(der: Prism<T>, debugLabel?: string): T {\r\n const _forceUpdate = useForceUpdate(debugLabel)\r\n\r\n const ref = useRef<QueueItem<T>>(undefined as $IntentionalAny)\r\n\r\n if (!ref.current) {\r\n lastOrder++\r\n\r\n ref.current = {\r\n order: lastOrder,\r\n runUpdate: () => {\r\n if (!ref.current.unmounted) {\r\n _forceUpdate()\r\n }\r\n },\r\n der,\r\n lastValue: undefined as $IntentionalAny,\r\n unmounted: false,\r\n queueUpdate: () => {\r\n if (TRACE) {\r\n ref.current.debug?.history.push(`queueUpdate()`)\r\n }\r\n pushToQueue(ref.current)\r\n },\r\n untap: der.onStale(() => {\r\n if (TRACE) {\r\n ref.current.debug!.history.push(`onStale(cb)`)\r\n }\r\n ref.current!.queueUpdate()\r\n }),\r\n }\r\n\r\n if (TRACE) {\r\n ref.current.debug = {\r\n label: debugLabel,\r\n traceOfFirstTimeRender: new Error(),\r\n history: [],\r\n }\r\n }\r\n }\r\n\r\n if (process.env.NODE_ENV !== 'production') {\r\n if (der !== ref.current.der) {\r\n console.error(\r\n 'Argument `der` in `usePrismInstance(der)` should not change between renders.',\r\n )\r\n }\r\n }\r\n\r\n useLayoutEffect(() => {\r\n return function onUnmount() {\r\n ref.current.unmounted = true\r\n ref.current.untap()\r\n removeFromQueue(ref.current)\r\n }\r\n }, [])\r\n\r\n // if we're queued but are rendering before our turn, remove us from the queue\r\n removeFromQueue(ref.current)\r\n\r\n const newValue = ref.current.der.getValue()\r\n ref.current.lastValue = newValue\r\n\r\n if (TRACE) {\r\n ref.current.debug?.history.push(`rendered`)\r\n }\r\n\r\n return newValue\r\n}\r\n\r\n/**\r\n * Creates a new Atom, similar to useState(), but the component\r\n * won't re-render if the value of the atom changes.\r\n *\r\n * @param initialState - Initial state\r\n * @returns The Atom\r\n *\r\n * @example\r\n *\r\n * Usage\r\n * ```tsx\r\n * import {useAtom, useVal} from '@tomorrowevening/theatre-react'\r\n * import {useEffect} from 'react'\r\n *\r\n * function MyComponent() {\r\n * const atom = useAtom({count: 0, ready: false})\r\n *\r\n * const onClick = () =>\r\n * atom.setByPointer(\r\n * (p) => p.count,\r\n * (count) => count + 1,\r\n * )\r\n *\r\n * useEffect(() => {\r\n * setTimeout(() => {\r\n * atom.setByPointer((p) => p.ready, true)\r\n * }, 1000)\r\n * }, [])\r\n *\r\n * const ready = useVal(atom.pointer.ready)\r\n * if (!ready) return <div>Loading...</div>\r\n * return <button onClick={onClick}>Click me</button>\r\n * }\r\n * ```\r\n */\r\nexport function useAtom<T>(initialState: T): Atom<T> {\r\n const ref = useRef<Atom<T>>(undefined as $IntentionalAny)\r\n if (!ref.current) {\r\n ref.current = new Atom(initialState)\r\n }\r\n return ref.current\r\n}\r\n"],
5
5
  "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@tomorrowevening/theatre-react",
3
- "version": "1.0.4",
3
+ "version": "1.0.8",
4
4
  "license": "Apache-2.0",
5
5
  "author": {
6
- "name": "Aria Minaei",
7
- "email": "aria@theatrejs.com",
8
- "url": "https://github.com/AriaMinaei"
6
+ "name": "Colin Duffy",
7
+ "email": "colin@tomorrowevening.com",
8
+ "url": "https://tomorrowevening.com/"
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/AriaMinaei/theatre",
12
+ "url": "https://github.com/tomorrowevening/theatre",
13
13
  "directory": "packages/react"
14
14
  },
15
15
  "main": "dist/index.js",