265 lines
6.5 KiB
TypeScript
265 lines
6.5 KiB
TypeScript
import type {IDerivation} from '@theatre/dataverse'
|
|
import {Box} from '@theatre/dataverse'
|
|
import {prism, val} from '@theatre/dataverse'
|
|
import {findIndex} from 'lodash-es'
|
|
import queueMicrotask from 'queue-microtask'
|
|
import {
|
|
useCallback,
|
|
useEffect,
|
|
useLayoutEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from 'react'
|
|
import {unstable_batchedUpdates} from 'react-dom'
|
|
|
|
type $IntentionalAny = any
|
|
type VoidFn = () => void
|
|
|
|
const logger = {log: console.log}
|
|
|
|
function useForceUpdate(debugLabel?: string) {
|
|
const [, setTick] = useState(0)
|
|
|
|
const update = useCallback(() => {
|
|
if (process.env.NODE_ENV !== 'production' && debugLabel)
|
|
logger.log(debugLabel, 'forceUpdate', {trace: new Error()})
|
|
|
|
setTick((tick) => tick + 1)
|
|
}, [])
|
|
|
|
return update
|
|
}
|
|
|
|
export function usePrism<T>(
|
|
fn: () => T,
|
|
deps: unknown[],
|
|
debugLabel?: string,
|
|
): T {
|
|
const fnAsCallback = useCallback(fn, deps)
|
|
const boxRef = useRef<Box<typeof fn>>(null as $IntentionalAny)
|
|
if (!boxRef.current) {
|
|
boxRef.current = new Box(fnAsCallback)
|
|
} else {
|
|
boxRef.current.set(fnAsCallback)
|
|
}
|
|
|
|
const derivation = useMemo(
|
|
() =>
|
|
prism(() => {
|
|
const fn = boxRef.current.derivation.getValue()
|
|
return fn()
|
|
}),
|
|
[],
|
|
)
|
|
|
|
return useDerivation(derivation, debugLabel)
|
|
}
|
|
|
|
export const useVal: typeof val = (p: $IntentionalAny, debugLabel?: string) => {
|
|
return usePrism(() => val(p), [p], debugLabel)
|
|
}
|
|
|
|
/**
|
|
* Each usePrism() call is assigned an `order`. Parents have a smaller
|
|
* order than their children, and so on.
|
|
*/
|
|
let lastOrder = 0
|
|
|
|
/**
|
|
* A sorted array of derivations that need to be refreshed. The derivations are sorted
|
|
* by their order, which means a parent derivation always gets priority to children
|
|
* and descendents. Ie. we refresh the derivations top to bottom.
|
|
*/
|
|
const queue: QueueItem[] = []
|
|
const setOfQueuedItems = new Set<QueueItem>()
|
|
|
|
type QueueItem = {
|
|
order: number
|
|
/**
|
|
* runUpdate() is the equivalent of a forceUpdate() call.
|
|
*/
|
|
runUpdate: VoidFn
|
|
debugLabel?: string
|
|
}
|
|
|
|
let microtaskIsQueued = false
|
|
|
|
const pushToQueue = (item: QueueItem) => {
|
|
_pushToQueue(item)
|
|
queueIfNeeded()
|
|
}
|
|
|
|
const _pushToQueue = (item: QueueItem) => {
|
|
if (setOfQueuedItems.has(item)) return
|
|
setOfQueuedItems.add(item)
|
|
|
|
if (queue.length === 0) {
|
|
queue.push(item)
|
|
} else {
|
|
const index = findIndex(
|
|
queue,
|
|
(existingItem) => existingItem.order >= item.order,
|
|
)
|
|
if (index === -1) {
|
|
queue.push(item)
|
|
} else {
|
|
const right = queue[index]
|
|
if (right.order > item.order) {
|
|
queue.splice(index, 0, item)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const removeFromQueue = (item: QueueItem) => {
|
|
if (!setOfQueuedItems.has(item)) return
|
|
setOfQueuedItems.delete(item)
|
|
|
|
const index = findIndex(queue, (o) => o === item)
|
|
queue.splice(index, 1)
|
|
}
|
|
|
|
function queueIfNeeded() {
|
|
if (!microtaskIsQueued) {
|
|
microtaskIsQueued = true
|
|
queueMicrotask(() => {
|
|
let i = 0
|
|
while (queue.length > 0) {
|
|
i++
|
|
if (i === 4) {
|
|
// react might be skipping updates, perhaps in concurrent mode.
|
|
//we can recheck the queue later
|
|
setTimeout(queueIfNeeded, 1)
|
|
break
|
|
}
|
|
unstable_batchedUpdates(() => {
|
|
for (const item of queue) {
|
|
item.runUpdate()
|
|
}
|
|
}, 1)
|
|
}
|
|
microtaskIsQueued = false
|
|
})
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @remarks
|
|
* It looks like this new implementation of useDerivation() manages to:
|
|
* 1. Not over-calculate the derivations
|
|
* 2. Render derivation in ancestor -\> descendent order
|
|
* 3. Not set off React's concurrent mode alarms
|
|
*
|
|
* It works like an implementation of Dataverse's Ticker, except that it runs
|
|
* the side effects in an order where a component's derivation is guaranteed
|
|
* to run before any of its descendents' derivations.
|
|
*
|
|
* I'm happy with how little bookkeeping we ended up doing here.
|
|
*/
|
|
function useDerivation<T>(der: IDerivation<T>, debugLabel?: string): T {
|
|
const _forceUpdate = useForceUpdate(debugLabel)
|
|
|
|
const refs = useRef<{queueItem: QueueItem; unmounted: boolean}>(
|
|
undefined as $IntentionalAny,
|
|
)
|
|
|
|
if (!refs.current) {
|
|
lastOrder++
|
|
refs.current = {
|
|
queueItem: {
|
|
debugLabel,
|
|
order: lastOrder,
|
|
runUpdate: () => {
|
|
if (!refs.current.unmounted) {
|
|
_forceUpdate()
|
|
}
|
|
},
|
|
},
|
|
unmounted: false,
|
|
}
|
|
}
|
|
|
|
const queueUpdate = useCallback(() => {
|
|
pushToQueue(refs.current.queueItem)
|
|
}, [])
|
|
|
|
useLayoutEffect(() => {
|
|
const untap = der.changesWithoutValues().tap(() => {
|
|
queueUpdate()
|
|
})
|
|
if (lastValueRef.current !== der.getValue()) {
|
|
queueUpdate()
|
|
}
|
|
|
|
return untap
|
|
}, [der])
|
|
|
|
useLayoutEffect(() => {
|
|
return function onUnmount() {
|
|
refs.current.unmounted = true
|
|
removeFromQueue(refs.current.queueItem)
|
|
}
|
|
}, [])
|
|
|
|
const lastValueRef = useRef<T>(undefined as $IntentionalAny as T)
|
|
const queueItem = refs.current.queueItem
|
|
|
|
// we defer refreshing our derivation if:
|
|
const mustDefer =
|
|
// we are actually queued to refresh
|
|
setOfQueuedItems.has(queueItem) &&
|
|
// but it's not our turn yet
|
|
queue[0] !== refs.current.queueItem
|
|
|
|
if (!mustDefer) {
|
|
removeFromQueue(queueItem)
|
|
lastValueRef.current = der.getValue()
|
|
} else {
|
|
// if it's not our turn, we return the last cached value,
|
|
// which react will actually drop, because the microtask
|
|
// queue will make sure to forceUpdate() us before react
|
|
// flushes to DOM.
|
|
}
|
|
|
|
return lastValueRef.current
|
|
}
|
|
|
|
/**
|
|
* This makes sure the prism derivation remains hot as long as the
|
|
* component calling the hook is alive, but it does not
|
|
* return the value of the derivation, and it does not
|
|
* re-render the component if the value of the derivation changes.
|
|
*
|
|
* Use this hook if you plan to read a derivation in a
|
|
* useEffect() call, without the derivation causing your
|
|
* element to re-render.
|
|
*/
|
|
export function usePrismWithoutReRender<T>(
|
|
fn: () => T,
|
|
deps: unknown[],
|
|
): IDerivation<T> {
|
|
const derivation = useMemo(() => prism(fn), deps)
|
|
|
|
return useDerivationWithoutReRender(derivation)
|
|
}
|
|
|
|
/**
|
|
* This makes sure the derivation remains hot as long as the
|
|
* component calling the hook is alive, but it does not
|
|
* return the value of the derivation, and it does not
|
|
* re-render the component if the value of the derivation changes.
|
|
*/
|
|
export function useDerivationWithoutReRender<T>(
|
|
der: IDerivation<T>,
|
|
): IDerivation<T> {
|
|
useEffect(() => {
|
|
const untap = der.keepHot()
|
|
|
|
return () => {
|
|
untap()
|
|
}
|
|
}, [der])
|
|
|
|
return der
|
|
}
|