Skip to content

Commit 0d71b81

Browse files
committed
types: update useTransition types
1 parent b793744 commit 0d71b81

File tree

3 files changed

+147
-59
lines changed

3 files changed

+147
-59
lines changed

packages/core/src/useTransition.tsx

Lines changed: 114 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,120 @@ import React, {
44
useLayoutEffect,
55
useImperativeHandle,
66
ReactNode,
7+
RefObject,
78
} from 'react'
8-
import { is, toArray, useForceUpdate, useOnce, each, OneOrMore } from 'shared'
9+
import {
10+
is,
11+
toArray,
12+
useForceUpdate,
13+
useOnce,
14+
each,
15+
OneOrMore,
16+
Falsy,
17+
Indexable,
18+
Merge,
19+
AnyKey,
20+
} from 'shared'
921
import { now } from 'shared/globals'
1022

1123
import { DEFAULT_PROPS, callProp, interpolateTo } from './helpers'
12-
import { SpringHandle, SpringProps } from './types/spring'
13-
import { Controller } from './Controller'
24+
import { SpringHandle, AsyncTo, FromProp, SpringValues } from './types/spring'
25+
import { Controller, ControllerProps } from './Controller'
26+
import { AnimationProps, AnimationEvents } from './types/animated'
27+
import { UseSpringProps } from './useSpring'
1428

1529
// TODO: convert to "const enum" once Babel supports it
16-
type Phase = number
30+
export type Phase = number & { __type: 'TransitionPhase' }
1731
/** This transition is being mounted */
18-
const MOUNT = 0
32+
const MOUNT = 0 as Phase
1933
/** This transition is entering or has entered */
20-
const ENTER = 1
34+
const ENTER = 1 as Phase
2135
/** This transition had its animations updated */
22-
const UPDATE = 2
36+
const UPDATE = 2 as Phase
2337
/** This transition will expire after animating */
24-
const LEAVE = 3
38+
const LEAVE = 3 as Phase
39+
40+
type UnknownProps = Indexable<any>
41+
42+
type PhaseProp<Item> =
43+
| Falsy
44+
| OneOrMore<UseSpringProps>
45+
| ((
46+
item: Item,
47+
index: number
48+
) => UseSpringProps | AsyncTo<UnknownProps> | Falsy)
49+
50+
type PhaseProps<Item = any, From = {}> = {
51+
from?: From &
52+
(
53+
| FromProp<UnknownProps>
54+
| ((item: Item, index: number) => FromProp<UnknownProps>))
55+
initial?: From &
56+
(
57+
| FromProp<UnknownProps>
58+
| ((item: Item, index: number) => FromProp<UnknownProps>))
59+
enter?: PhaseProp<Item>
60+
update?: PhaseProp<Item>
61+
leave?: PhaseProp<Item>
62+
}
2563

26-
export type UseTransitionProps<T> = { [key: string]: any | T } // TODO
27-
export type ItemsProp<T> = ReadonlyArray<T> | T | null | undefined
2864
export type ItemKeys<T> =
29-
| ((item: T) => string | number)
30-
| ReadonlyArray<string | number>
31-
| string
32-
| number
65+
| AnyKey
66+
| ReadonlyArray<AnyKey>
67+
| ((item: T) => AnyKey)
3368
| null
3469

35-
export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
70+
export type UseTransitionProps<Item = any> = Merge<
71+
AnimationProps & AnimationEvents,
72+
{
73+
/**
74+
* Used to access the imperative API.
75+
*
76+
* Animations never auto-start when `ref` is defined.
77+
*/
78+
ref?: RefObject<TransitionHandle>
79+
key?: ItemKeys<Item>
80+
sort?: (a: Item, b: Item) => number
81+
trail?: number
82+
expires?: number
83+
}
84+
>
85+
86+
/** The imperative `ref` API */
87+
export type TransitionHandle = Merge<
88+
SpringHandle,
89+
{
90+
update(props: ControllerProps): TransitionHandle
91+
}
92+
>
93+
94+
/** The function returned by `useTransition` */
95+
export interface TransitionFn<Item = any, Values extends object = any> {
96+
(
97+
render: (
98+
values: Values,
99+
item: Item,
100+
transition: TransitionState<Item>
101+
) => ReactNode
102+
): ReactNode[]
103+
}
104+
105+
export function useTransition<Item, From, Props extends object>(
106+
data: OneOrMore<Item>,
107+
props: Props & PhaseProps<Item, From> & UseTransitionProps<Item>,
108+
deps?: any[]
109+
): TransitionFn<Item, SpringValues<Props>>
110+
111+
export function useTransition(
112+
data: unknown,
113+
props: PhaseProps & UseTransitionProps,
114+
deps?: any[]
115+
): TransitionFn {
36116
const { key, ref, reset, sort, trail = 0, expires = Infinity } = props
37117

38118
// Every item has its own transition.
39-
const items = toArray<unknown>(data)
40-
const transitions: Transition[] = []
119+
const items = toArray(data)
120+
const transitions: TransitionState[] = []
41121

42122
// Keys help with reusing transitions between renders.
43123
// The `key` prop can be undefined (which means the items themselves are used
@@ -50,7 +130,7 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
50130
: toArray(key)
51131

52132
// The "onRest" callbacks need a ref to the latest transitions.
53-
const usedTransitions = useRef<Transition[] | null>(null)
133+
const usedTransitions = useRef<TransitionState[] | null>(null)
54134
const prevTransitions = usedTransitions.current
55135
useLayoutEffect(() => {
56136
usedTransitions.current = transitions
@@ -59,7 +139,9 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
59139
// Destroy all transitions on dismount.
60140
useOnce(() => () =>
61141
each(usedTransitions.current!, t => {
62-
if (t.expiresBy) clearTimeout(t.expirationId)
142+
if (t.expiresBy != null) {
143+
clearTimeout(t.expirationId)
144+
}
63145
t.ctrl.dispose()
64146
})
65147
)
@@ -69,7 +151,7 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
69151
if (prevTransitions && !reset)
70152
each(prevTransitions, (t, i) => {
71153
// Expired transitions are not rendered.
72-
if (t.expiresBy) {
154+
if (t.expiresBy != null) {
73155
clearTimeout(t.expirationId)
74156
} else {
75157
i = reused[i] = keys.indexOf(t.key)
@@ -113,15 +195,15 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
113195
// Expired transitions use this to dismount.
114196
const forceUpdate = useForceUpdate()
115197

116-
const defaultProps: any = {}
198+
const defaultProps = {} as UnknownProps
117199
each(DEFAULT_PROPS, prop => {
118200
if (/function|object/.test(typeof props[prop])) {
119201
defaultProps[prop] = props[prop]
120202
}
121203
})
122204

123205
// Generate changes to apply in useEffect.
124-
const changes = new Map<Transition<T>, Change>()
206+
const changes = new Map<TransitionState, Change>()
125207
each(transitions, (t, i) => {
126208
let to: any
127209
let from: any
@@ -151,7 +233,7 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
151233
}
152234

153235
// The payload is used to update the spring props once the current render is committed.
154-
const payload = {
236+
const payload: ControllerProps = {
155237
...defaultProps,
156238
// When "to" is a function, it can return (1) an array of "useSpring" props,
157239
// (2) an async function, or (3) an object with any "useSpring" props.
@@ -160,7 +242,7 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
160242
delay: delay += trail,
161243
config: callProp(props.config || defaultProps.config, t.item, i),
162244
...(is.obj(to) && interpolateTo(to)),
163-
} as SpringProps
245+
}
164246

165247
const { onRest } = payload
166248
payload.onRest = result => {
@@ -176,7 +258,9 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
176258
const transitions = usedTransitions.current!
177259
if (transitions.every(t => t.ctrl.idle)) {
178260
forceUpdate()
179-
} else if (expires < Infinity) {
261+
}
262+
// When `expires` is infinite, postpone dismount until next render.
263+
else if (expires < Infinity) {
180264
t.expirationId = setTimeout(forceUpdate, expires)
181265
}
182266
}
@@ -196,7 +280,7 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
196280
})
197281

198282
const api = useMemo(
199-
(): SpringHandle => ({
283+
(): TransitionHandle => ({
200284
get controllers() {
201285
return usedTransitions.current!.map(t => t.ctrl)
202286
},
@@ -234,9 +318,9 @@ export function useTransition<T>(data: OneOrMore<T>, props: any, deps?: any) {
234318
reset ? void 0 : deps
235319
)
236320

237-
return (render: (props: any, item: T) => ReactNode) =>
321+
return render =>
238322
transitions.map(t => {
239-
const elem: any = render({ ...t.ctrl.springs }, t.item)
323+
const elem: any = render({ ...t.ctrl.springs } as any, t.item, t)
240324
return elem && elem.type ? (
241325
<elem.type
242326
{...elem.props}
@@ -254,9 +338,9 @@ interface Change {
254338
payload?: any
255339
}
256340

257-
interface Transition<T = any> {
341+
export interface TransitionState<Item = any> {
258342
key: any
259-
item: T
343+
item: Item
260344
ctrl: Controller
261345
phase: Phase
262346
/** Destroy no later than this date */

targets/web/src/types/__tests__/Transition.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react';
22
import { assert, test, _ } from 'spec.ts';
3-
import { Transition, animated, SpringValue, TransitionPhase } from '../..';
4-
import { SpringValues } from '@react-spring/core';
3+
import { SpringValues, Transition, TransitionPhase } from '@react-spring/core';
4+
import { animated } from '../..';
55

66
const View = animated('div');
77

targets/web/src/types/__tests__/useTransition.tsx

Lines changed: 31 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,53 @@
11
import React from 'react';
22
import { assert, test, _ } from 'spec.ts';
3-
import {
4-
animated,
5-
useTransition,
6-
ItemTransition,
7-
SpringValue,
8-
SpringUpdateFn,
9-
} from '../..';
3+
import { SpringValues } from '@react-spring/core';
4+
import { animated, useTransition, SpringUpdateFn } from '../..';
105

116
const View = animated('div');
127

138
const items = [1, 2] as [1, 2];
149

1510
test('infer animated from these props', () => {
16-
const [transition] = useTransition(items, null, {
11+
const transition = useTransition(items, {
1712
from: { a: 1 },
1813
enter: { b: 1 },
1914
leave: { c: 1 },
2015
update: { d: 1 },
2116
initial: { e: 1 },
2217
});
23-
assert(transition.props, _ as {
24-
[key: string]: SpringValue<any>;
25-
a: SpringValue<number>;
26-
b: SpringValue<number>;
27-
c: SpringValue<number>;
28-
d: SpringValue<number>;
29-
e: SpringValue<number>;
18+
transition((style, item) => {
19+
assert(style, _ as SpringValues<{
20+
a: number;
21+
b: number;
22+
c: number;
23+
d: number;
24+
e: number;
25+
}>);
26+
assert(item, _ as 1 | 2);
27+
return null;
3028
});
3129
});
3230

3331
test('basic usage', () => {
34-
const transitions = useTransition(items, null, {
32+
const transition = useTransition(items, {
3533
from: { opacity: 0 },
3634
enter: [{ opacity: 1 }, { color: 'red' }],
3735
leave: { opacity: 0 },
3836
});
3937

4038
// You typically map transition objects into JSX elements.
41-
return transitions.map(transition => {
42-
type T = ItemTransition<1 | 2, { opacity: number; color: string }>;
43-
assert(transition, _ as T);
44-
return <View style={transition.props}>{transition.item}</View>;
39+
return transition((style, item) => {
40+
assert(style, _ as SpringValues<{
41+
opacity?: number; // FIXME: "opacity" should never be undefined because it exists in "from"
42+
color?: string;
43+
}>);
44+
assert(item, _ as 1 | 2);
45+
return <View style={style}>{item}</View>;
4546
});
4647
});
4748

4849
test('with function props', () => {
49-
const transitions = useTransition(items, null, {
50+
const transition = useTransition(items, {
5051
from: item => {
5152
assert(item, _ as 1 | 2);
5253
return { width: 0, height: 0 };
@@ -57,15 +58,18 @@ test('with function props', () => {
5758
},
5859
leave: { width: '0%', opacity: 0 },
5960
});
60-
assert(transitions[0].props, _ as {
61-
[key: string]: SpringValue<any>;
62-
width: SpringValue<string | number>;
63-
height: SpringValue<string | number>;
64-
opacity: SpringValue<number>;
61+
transition((style, item) => {
62+
assert(style, _ as SpringValues<{
63+
width: string | number;
64+
height: string;
65+
opacity: number;
66+
}>);
67+
assert(item, _ as 1 | 2);
68+
return null;
6569
});
6670

6771
test('return an async function', () => {
68-
useTransition(items, null, {
72+
useTransition(items, {
6973
update: item => async next => {
7074
assert(item, _ as 1 | 2);
7175
assert(next, _ as SpringUpdateFn); // FIXME: should be "SpringUpdateFn<{ opacity: number, ... }>"

0 commit comments

Comments
 (0)