Renamed @theatre/dataverse-react to @theatre/react
This commit is contained in:
parent
69a12440ca
commit
1452c9ebbe
60 changed files with 80 additions and 80 deletions
265
packages/react/src/index.ts
Normal file
265
packages/react/src/index.ts
Normal file
|
@ -0,0 +1,265 @@
|
|||
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
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue