-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathwithAnimated.tsx
138 lines (115 loc) · 3.75 KB
/
withAnimated.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import * as React from 'react'
import { forwardRef, useRef, Ref, useCallback, useEffect } from 'react'
import {
is,
each,
raf,
useForceUpdate,
useOnce,
FluidEvent,
FluidValue,
addFluidObserver,
removeFluidObserver,
useIsomorphicLayoutEffect,
} from '@react-spring/shared'
import { ElementType } from '@react-spring/types'
import { AnimatedObject } from './AnimatedObject'
import { TreeContext } from './context'
import { HostConfig } from './createHost'
export type AnimatableComponent = string | Exclude<ElementType, string>
export const withAnimated = (Component: any, host: HostConfig) => {
const hasInstance: boolean =
// Function components must use "forwardRef" to avoid being
// re-rendered on every animation frame.
!is.fun(Component) ||
(Component.prototype && Component.prototype.isReactComponent)
return forwardRef((givenProps: any, givenRef: Ref<any>) => {
const instanceRef = useRef<any>(null)
// The `hasInstance` value is constant, so we can safely avoid
// the `useCallback` invocation when `hasInstance` is false.
const ref =
hasInstance &&
// eslint-disable-next-line react-hooks/rules-of-hooks
useCallback(
(value: any) => {
instanceRef.current = updateRef(givenRef, value)
},
[givenRef]
)
const [props, deps] = getAnimatedState(givenProps, host)
const forceUpdate = useForceUpdate()
const callback = () => {
const instance = instanceRef.current
if (hasInstance && !instance) {
// Either this component was unmounted before changes could be
// applied, or the wrapped component forgot to forward its ref.
return
}
const didUpdate = instance
? host.applyAnimatedValues(instance, props.getValue(true))
: false
// Re-render the component when native updates fail.
if (didUpdate === false) {
forceUpdate()
}
}
const observer = new PropsObserver(callback, deps)
const observerRef = useRef<PropsObserver>(null)
useIsomorphicLayoutEffect(() => {
observerRef.current = observer
// Observe the latest dependencies.
each(deps, dep => addFluidObserver(dep, observer))
return () => {
// Stop observing previous dependencies.
if (observerRef.current) {
each(observerRef.current.deps, dep =>
removeFluidObserver(dep, observerRef.current!)
)
raf.cancel(observerRef.current.update)
}
}
})
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(callback, [])
// Stop observing on unmount.
useOnce(() => () => {
const observer = observerRef.current!
each(observer.deps, dep => removeFluidObserver(dep, observer))
})
const usedProps = host.getComponentProps(props.getValue())
return <Component {...usedProps} ref={ref} />
})
}
class PropsObserver {
constructor(
readonly update: () => void,
readonly deps: Set<FluidValue>
) {}
eventObserved(event: FluidEvent) {
if (event.type == 'change') {
raf.write(this.update)
}
}
}
type AnimatedState = [props: AnimatedObject, dependencies: Set<FluidValue>]
function getAnimatedState(props: any, host: HostConfig): AnimatedState {
const dependencies = new Set<FluidValue>()
TreeContext.dependencies = dependencies
// Search the style for dependencies.
if (props.style)
props = {
...props,
style: host.createAnimatedStyle(props.style),
}
// Search the props for dependencies.
props = new AnimatedObject(props)
TreeContext.dependencies = null
return [props, dependencies]
}
function updateRef<T>(ref: Ref<T>, value: T) {
if (ref) {
if (is.fun(ref)) ref(value)
else (ref as any).current = value
}
return value
}