Skip to content

Commit c133086

Browse files
authored
Convert victory-animation to function component (#2788)
1 parent 0cea960 commit c133086

File tree

2 files changed

+115
-141
lines changed

2 files changed

+115
-141
lines changed

.changeset/eleven-geckos-trade.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"victory-core": patch
3+
---
4+
5+
Convert victory-animation to function component
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
/* global setTimeout:false */
21
import React from "react";
32
import * as d3Ease from "victory-vendor/d3-ease";
43
import { victoryInterpolator } from "./util";
54
import TimerContext from "../victory-util/timer-context";
6-
import isEqual from "react-fast-compare";
7-
import type Timer from "../victory-util/timer";
85

96
/**
107
* Single animation object to interpolate
@@ -15,6 +12,7 @@ export type AnimationStyle = { [key: string]: string | number };
1512
*/
1613

1714
export type AnimationData = AnimationStyle | AnimationStyle[];
15+
1816
export type AnimationEasing =
1917
| "back"
2018
| "backIn"
@@ -58,17 +56,19 @@ export type AnimationEasing =
5856
| "sinInOut";
5957

6058
export interface VictoryAnimationProps {
61-
children: (style: AnimationStyle, info: AnimationInfo) => React.ReactNode;
59+
children: (style: AnimationStyle, info: AnimationInfo) => React.ReactElement;
6260
duration?: number;
6361
easing?: AnimationEasing;
6462
delay?: number;
6563
onEnd?: () => void;
6664
data: AnimationData;
6765
}
66+
6867
export interface VictoryAnimationState {
6968
data: AnimationStyle;
7069
animationInfo: AnimationInfo;
7170
}
71+
7272
export interface AnimationInfo {
7373
progress: number;
7474
animating: boolean;
@@ -79,169 +79,138 @@ export interface VictoryAnimation {
7979
context: React.ContextType<typeof TimerContext>;
8080
}
8181

82-
export class VictoryAnimation extends React.Component<
83-
VictoryAnimationProps,
84-
VictoryAnimationState
85-
> {
86-
static displayName = "VictoryAnimation";
87-
88-
static defaultProps = {
89-
data: {},
90-
delay: 0,
91-
duration: 1000,
92-
easing: "quadInOut",
93-
};
82+
/** d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc. */
83+
const formatAnimationName = (name: AnimationEasing) => {
84+
const capitalizedName = name.charAt(0).toUpperCase() + name.slice(1);
85+
return `ease${capitalizedName}`;
86+
};
9487

95-
static contextType = TimerContext;
96-
private interpolator: null | ((value: number) => AnimationStyle);
97-
private queue: AnimationStyle[];
98-
private ease: any;
99-
private timer: Timer;
100-
private loopID?: number;
101-
102-
constructor(props, context) {
103-
super(props, context);
104-
/* defaults */
105-
this.state = {
106-
data: Array.isArray(this.props.data)
107-
? this.props.data[0]
108-
: this.props.data,
109-
animationInfo: {
110-
progress: 0,
111-
animating: false,
112-
},
113-
};
114-
this.interpolator = null;
115-
this.queue = Array.isArray(this.props.data) ? this.props.data.slice(1) : [];
116-
/* build easing function */
117-
this.ease = d3Ease[this.toNewName(this.props.easing)];
118-
this.timer = this.context.animationTimer;
119-
}
120-
121-
componentDidMount() {
88+
const DEFAULT_DURATION = 1000;
89+
90+
export const VictoryAnimation = ({
91+
duration = DEFAULT_DURATION,
92+
easing = "quadInOut",
93+
delay = 0,
94+
data,
95+
children,
96+
onEnd,
97+
}: VictoryAnimationProps) => {
98+
const [state, setState] = React.useState<VictoryAnimationState>({
99+
data: Array.isArray(data) ? data[0] : data,
100+
animationInfo: {
101+
progress: 0,
102+
animating: false,
103+
},
104+
});
105+
106+
const timer = React.useContext(TimerContext).animationTimer;
107+
const queue = React.useRef<AnimationStyle[]>(
108+
Array.isArray(data) ? data.slice(1) : [],
109+
);
110+
const interpolator = React.useRef<null | ((value: number) => AnimationStyle)>(
111+
null,
112+
);
113+
const loopID = React.useRef<number | undefined>(undefined);
114+
const ease = d3Ease[formatAnimationName(easing)];
115+
116+
React.useEffect(() => {
122117
// Length check prevents us from triggering `onEnd` in `traverseQueue`.
123-
if (this.queue.length) {
124-
this.traverseQueue();
118+
if (queue.current.length) {
119+
traverseQueue();
125120
}
126-
}
127-
128-
componentDidUpdate(prevProps) {
129-
const equalProps = isEqual(this.props, prevProps);
130-
if (!equalProps) {
131-
/* If the previous animation didn't finish, force it to complete before starting a new one */
132-
if (
133-
this.interpolator &&
134-
this.state.animationInfo &&
135-
this.state.animationInfo.progress < 1
136-
) {
137-
// eslint-disable-next-line react/no-did-update-set-state
138-
this.setState({
139-
data: this.interpolator(1),
140-
animationInfo: {
141-
progress: 1,
142-
animating: false,
143-
terminating: true,
144-
},
145-
});
121+
122+
// Clean up the animation loop
123+
return () => {
124+
if (loopID.current) {
125+
timer.unsubscribe(loopID.current);
146126
} else {
147-
/* cancel existing loop if it exists */
148-
this.timer.unsubscribe(this.loopID);
149-
/* If an object was supplied */
150-
if (!Array.isArray(this.props.data)) {
151-
// Replace the tween queue. Could set `this.queue = [nextProps.data]`,
152-
// but let's reuse the same array.
153-
this.queue.length = 0;
154-
this.queue.push(this.props.data);
155-
/* If an array was supplied */
156-
} else {
157-
/* Extend the tween queue */
158-
this.queue.push(...this.props.data);
159-
}
160-
/* Start traversing the tween queue */
161-
this.traverseQueue();
127+
timer.stop();
162128
}
163-
}
164-
}
129+
};
130+
// eslint-disable-next-line react-hooks/exhaustive-deps
131+
}, []);
165132

166-
componentWillUnmount() {
167-
if (this.loopID) {
168-
this.timer.unsubscribe(this.loopID);
133+
React.useEffect(() => {
134+
// If the previous animation didn't finish, force it to complete before starting a new one
135+
if (
136+
interpolator.current &&
137+
state.animationInfo &&
138+
state.animationInfo.progress < 1
139+
) {
140+
setState({
141+
data: interpolator.current(1),
142+
animationInfo: {
143+
progress: 1,
144+
animating: false,
145+
terminating: true,
146+
},
147+
});
169148
} else {
170-
this.timer.stop();
149+
// Cancel existing loop if it exists
150+
timer.unsubscribe(loopID.current);
151+
// Set the tween queue to the new data
152+
queue.current = Array.isArray(data) ? data : [data];
153+
// Start traversing the tween queue
154+
traverseQueue();
171155
}
172-
}
173-
174-
toNewName(ease) {
175-
// d3-ease changed the naming scheme for ease from "linear" -> "easeLinear" etc.
176-
const capitalize = (s) => s && s[0].toUpperCase() + s.slice(1);
177-
return `ease${capitalize(ease)}`;
178-
}
179-
180-
/* Traverse the tween queue */
181-
traverseQueue() {
182-
if (this.queue.length) {
183-
/* Get the next index */
184-
const data = this.queue[0];
185-
/* compare cached version to next props */
186-
this.interpolator = victoryInterpolator(this.state.data, data);
187-
/* reset step to zero */
188-
if (this.props.delay) {
156+
// eslint-disable-next-line react-hooks/exhaustive-deps
157+
}, [data]);
158+
159+
const traverseQueue = () => {
160+
if (queue.current.length) {
161+
const nextData = queue.current[0];
162+
163+
// Compare cached version to next props
164+
interpolator.current = victoryInterpolator(state.data, nextData);
165+
166+
// Reset step to zero
167+
if (delay) {
189168
setTimeout(() => {
190-
this.loopID = this.timer.subscribe(
191-
this.functionToBeRunEachFrame,
192-
this.props.duration!,
193-
);
194-
}, this.props.delay);
169+
loopID.current = timer.subscribe(functionToBeRunEachFrame, duration);
170+
}, delay);
195171
} else {
196-
this.loopID = this.timer.subscribe(
197-
this.functionToBeRunEachFrame,
198-
this.props.duration!,
199-
);
172+
loopID.current = timer.subscribe(functionToBeRunEachFrame, duration);
200173
}
201-
} else if (this.props.onEnd) {
202-
this.props.onEnd();
174+
} else if (onEnd) {
175+
onEnd();
203176
}
204-
}
205-
/* every frame we... */
206-
functionToBeRunEachFrame = (elapsed, duration) => {
207-
/*
208-
step can generate imprecise values, sometimes greater than 1
209-
if this happens set the state to 1 and return, cancelling the timer
210-
*/
211-
const animationDuration =
212-
duration !== undefined ? duration : this.props.duration;
213-
const step = animationDuration ? elapsed / animationDuration : 1;
177+
};
178+
179+
const functionToBeRunEachFrame = (elapsed: number) => {
180+
if (!interpolator.current) return;
181+
182+
// Step can generate imprecise values, sometimes greater than 1
183+
// if this happens set the state to 1 and return, cancelling the timer
184+
const step = duration ? elapsed / duration : 1;
185+
214186
if (step >= 1) {
215-
this.setState({
216-
data: this.interpolator!(1),
187+
setState({
188+
data: interpolator.current(1),
217189
animationInfo: {
218190
progress: 1,
219191
animating: false,
220192
terminating: true,
221193
},
222194
});
223-
if (this.loopID) {
224-
this.timer.unsubscribe(this.loopID);
195+
if (loopID.current) {
196+
timer.unsubscribe(loopID.current);
225197
}
226-
this.queue.shift();
227-
this.traverseQueue();
198+
queue.current.shift();
199+
traverseQueue();
228200
return;
229201
}
230-
/*
231-
if we're not at the end of the timer, set the state by passing
232-
current step value that's transformed by the ease function to the
233-
interpolator, which is cached for performance whenever props are received
234-
*/
235-
this.setState({
236-
data: this.interpolator!(this.ease(step)),
202+
203+
// If we're not at the end of the timer, set the state by passing
204+
// current step value that's transformed by the ease function to the
205+
// interpolator, which is cached for performance whenever props are received
206+
setState({
207+
data: interpolator.current(ease(step)),
237208
animationInfo: {
238209
progress: step,
239210
animating: step < 1,
240211
},
241212
});
242213
};
243214

244-
render() {
245-
return this.props.children(this.state.data, this.state.animationInfo);
246-
}
247-
}
215+
return children(state.data, state.animationInfo);
216+
};

0 commit comments

Comments
 (0)