Skip to content

Commit 92c0e48

Browse files
committed
refactor: useSprings
- The "update" and "stop" functions never change - Better support for changing number of springs between renders - Reorganized code
1 parent 38f952b commit 92c0e48

File tree

2 files changed

+62
-61
lines changed

2 files changed

+62
-61
lines changed

src/shared/helpers.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MutableRefObject, Ref, useCallback, useState } from 'react'
1+
import { MutableRefObject, Ref, useCallback, useState, useRef } from 'react'
22

33
export type NarrowObject<T> = unknown extends T
44
? T & { [key: string]: any }
@@ -133,6 +133,14 @@ export function handleRef<T>(ref: T, forward: Ref<T>) {
133133
return ref
134134
}
135135

136+
/** Use a value from the previous render */
137+
export function usePrev<T>(value: T): T | undefined {
138+
const prevRef = useRef<any>(undefined)
139+
const prev = prevRef.current
140+
prevRef.current = value
141+
return prev
142+
}
143+
136144
export function fillArray<T>(length: number, mapIndex: (index: number) => T) {
137145
const arr = []
138146
for (let i = 0; i < length; i++) arr.push(mapIndex(i))

src/useSprings.js

Lines changed: 53 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,90 +1,83 @@
11
import { useMemo, useRef, useImperativeHandle, useEffect } from 'react'
2-
import Ctrl from './animated/Controller'
3-
import { callProp, fillArray, is, toArray } from './shared/helpers'
2+
import { callProp, fillArray, is, toArray, usePrev } from './shared/helpers'
3+
import Controller from './animated/Controller'
44

55
/** API
66
* const props = useSprings(number, [{ ... }, { ... }, ...])
77
* const [props, set] = useSprings(number, (i, controller) => ({ ... }))
88
*/
99

1010
export const useSprings = (length, propsArg) => {
11-
const mounted = useRef(false)
12-
const ctrl = useRef()
11+
const hasNewSprings = length !== usePrev(length)
1312
const isFn = is.fun(propsArg)
1413

1514
// The `propsArg` coerced into an array
1615
const props = isFn ? [] : propsArg
1716

18-
// The controller maintains the animation values, starts and stops animations
19-
const [controllers, setProps, ref, api] = useMemo(() => {
20-
let ref, controllers
21-
return [
22-
// Recreate the controllers whenever `length` changes
23-
(controllers = fillArray(length, i => {
24-
const c = new Ctrl()
25-
const p = props[i] || (props[i] = callProp(propsArg, i, c))
26-
if (i === 0) ref = p.ref
27-
return c.update(p)
28-
})),
29-
// This updates the controllers with new props
30-
props => {
17+
// Recreate the controllers whenever `length` changes
18+
const springsRef = useRef()
19+
const springs = useMemo(
20+
() =>
21+
fillArray(length, i => {
22+
const s = new Controller()
23+
const p = props[i] || (props[i] = callProp(propsArg, i, s))
24+
return s.update(p)
25+
}),
26+
[length]
27+
)
28+
29+
const ref = springs[0].props.ref
30+
const { start, update, stop } = useMemo(
31+
() => ({
32+
/** Apply any pending updates */
33+
start: () =>
34+
Promise.all(
35+
springsRef.current.map(s => new Promise(done => s.start(done)))
36+
),
37+
/** Update the spring controllers */
38+
update: props => {
3139
const isFn = is.fun(props)
3240
if (!isFn) props = toArray(props)
33-
controllers.forEach((c, i) => {
34-
c.update(isFn ? callProp(props, i, c) : props[i])
35-
if (!ref) c.start()
41+
springsRef.current.forEach((spring, i) => {
42+
spring.update(isFn ? callProp(props, i, spring) : props[i])
43+
if (!ref) spring.start()
3644
})
3745
},
38-
// The imperative API is accessed via ref
39-
ref,
40-
ref && {
41-
start: () =>
42-
Promise.all(controllers.map(c => new Promise(r => c.start(r)))),
43-
stop: finished => controllers.forEach(c => c.stop(finished)),
44-
controllers,
45-
},
46-
]
47-
}, [length])
46+
/** Stop one key or all keys from animating */
47+
stop: (...args) => springsRef.current.forEach(s => s.stop(...args)),
48+
}),
49+
[]
50+
)
4851

49-
// Attach the imperative API to its ref
50-
useImperativeHandle(ref, () => api, [api])
52+
useImperativeHandle(ref, () => ({ start, stop }))
5153

5254
// Once mounted, update the local state and start any animations.
5355
useEffect(() => {
54-
if (!isFn || ctrl.current !== controllers) {
55-
controllers.forEach((c, i) => {
56-
const p = props[i]
57-
// Set the default props for async updates
58-
c.setProp('config', p.config)
59-
c.setProp('immediate', p.immediate)
56+
if (!isFn || hasNewSprings) {
57+
props.forEach((p, i) => {
58+
// Set default props for async updates
59+
springs[i].setProp('config', p.config)
60+
springs[i].setProp('immediate', p.immediate)
6061
})
6162
}
62-
63-
if (ctrl.current !== controllers) {
64-
if (ctrl.current) ctrl.current.forEach(c => c.destroy())
65-
ctrl.current = controllers
66-
}
67-
68-
if (mounted.current) {
69-
if (!isFn) setProps(props)
70-
} else if (!ref) {
71-
controllers.forEach(c => c.start())
63+
if (hasNewSprings) {
64+
if (springsRef.current) {
65+
springsRef.current.forEach(s => s.destroy())
66+
}
67+
springsRef.current = springs
68+
if (!ref) {
69+
springs.forEach(s => s.start())
70+
}
71+
} else if (!isFn) {
72+
update(props)
7273
}
7374
})
7475

75-
// Update mounted flag and destroy controller on unmount
76+
// Destroy the controllers on unmount
7677
useEffect(() => {
77-
mounted.current = true
78-
return () => ctrl.current.forEach(c => c.destroy())
78+
return () => springsRef.current.forEach(s => s.destroy())
7979
}, [])
8080

81-
// Return animated props, or, anim-props + the update-setter above
82-
const animatedProps = controllers.map(c => c.animated)
83-
return isFn
84-
? [
85-
animatedProps,
86-
setProps,
87-
(...args) => ctrl.current.forEach(c => c.stop(...args)),
88-
]
89-
: animatedProps
81+
const values = springs.map(s => s.animated)
82+
return isFn ? [values, update, stop] : values
9083
}

0 commit comments

Comments
 (0)