Skip to content

Commit e24d484

Browse files
authored
refactor(TextRotater): rewrite class component to functional (#8172)
1 parent 50b6c0e commit e24d484

File tree

1 file changed

+74
-89
lines changed

1 file changed

+74
-89
lines changed
Lines changed: 74 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,89 @@
11
import { clsx } from "clsx";
22
import PropTypes from "prop-types";
3-
import { Children, PureComponent, cloneElement } from "react";
3+
import {
4+
Children,
5+
cloneElement,
6+
memo,
7+
useCallback,
8+
useEffect,
9+
useRef,
10+
useState,
11+
} from "react";
412

5-
export default class TextRotater extends PureComponent {
6-
static defaultProps = {
7-
delay: 0,
8-
repeatDelay: 3000,
9-
};
13+
function TextRotater({ children, delay = 0, repeatDelay = 3000, maxWidth }) {
14+
const [currentIndex, setCurrentIndex] = useState(0);
15+
const [contentHeight, setContentHeight] = useState(0);
16+
const [isAnimating, setIsAnimating] = useState(false);
17+
const contentNodeRef = useRef(null);
18+
const heightTimeoutRef = useRef(null);
19+
const animationTimeoutRef = useRef(null);
20+
const repeatTimeoutRef = useRef(null);
1021

11-
static propTypes = {
12-
children: PropTypes.arrayOf(PropTypes.node),
13-
delay: PropTypes.number,
14-
repeatDelay: PropTypes.number,
15-
// Needed to prevent jump when
16-
// rotating between texts of different widths
17-
maxWidth: PropTypes.number,
18-
};
22+
const contentCallbackRef = useCallback((node) => {
23+
contentNodeRef.current = node;
24+
}, []);
1925

20-
state = {
21-
currentIndex: 0,
22-
contentHeight: 0,
23-
isAnimating: false,
24-
};
26+
useEffect(() => {
27+
const calculateHeight = () => {
28+
if (contentNodeRef.current) {
29+
setContentHeight(contentNodeRef.current.clientHeight);
30+
}
31+
};
2532

26-
render() {
27-
const { children, maxWidth } = this.props;
28-
const { currentIndex, contentHeight, isAnimating } = this.state;
33+
heightTimeoutRef.current = setTimeout(calculateHeight, 50);
34+
animationTimeoutRef.current = setTimeout(() => setIsAnimating(true), delay);
35+
36+
window.addEventListener("resize", calculateHeight);
37+
38+
return () => {
39+
clearTimeout(heightTimeoutRef.current);
40+
clearTimeout(animationTimeoutRef.current);
41+
clearTimeout(repeatTimeoutRef.current);
42+
window.removeEventListener("resize", calculateHeight);
43+
};
44+
}, []); // eslint-disable-line react-hooks/exhaustive-deps
45+
46+
const handleTransitionEnd = useCallback(() => {
2947
const childrenCount = Children.count(children);
48+
setCurrentIndex((prev) => (prev + 1) % childrenCount);
49+
setIsAnimating(false);
3050

31-
const currentChild = cloneElement(children[currentIndex], {
32-
ref: (child) => (this.content = child),
33-
});
51+
repeatTimeoutRef.current = setTimeout(() => {
52+
setIsAnimating(true);
53+
}, repeatDelay);
54+
}, [children, repeatDelay]);
3455

35-
const nextChild = cloneElement(
36-
children[(currentIndex + 1) % childrenCount],
37-
);
56+
const childrenCount = Children.count(children);
57+
const nextChild = cloneElement(children[(currentIndex + 1) % childrenCount]);
3858

39-
return (
59+
return (
60+
<div
61+
className="
62+
relative inline-block overflow-hidden align-bottom px-[0.3em]
63+
"
64+
>
4065
<div
41-
className="
42-
relative inline-block overflow-hidden align-bottom px-[0.3em]
43-
"
66+
className={clsx(
67+
"inline-flex flex-col text-left",
68+
isAnimating && "text-rotater--slide-up",
69+
)}
70+
onTransitionEnd={handleTransitionEnd}
71+
style={{ height: contentHeight, width: maxWidth }}
4472
>
45-
<div
46-
className={clsx(
47-
"inline-flex flex-col text-left",
48-
isAnimating && "text-rotater--slide-up",
49-
)}
50-
onTransitionEnd={this._handleTransitionEnd}
51-
style={{ height: contentHeight, width: maxWidth }}
52-
>
53-
{currentChild}
54-
{nextChild}
55-
</div>
73+
<span ref={contentCallbackRef}>{children[currentIndex]}</span>
74+
{nextChild}
5675
</div>
57-
);
58-
}
59-
60-
componentDidMount() {
61-
const { delay } = this.props;
62-
63-
this.heightTimeout = setTimeout(() => {
64-
this._calculateContentHeight();
65-
}, 50);
66-
67-
this.animationTimeout = setTimeout(() => {
68-
this.setState({ isAnimating: true });
69-
}, delay);
70-
71-
window.addEventListener("resize", this._calculateContentHeight);
72-
}
73-
74-
componentWillUnmount() {
75-
clearTimeout(this.heightTimeout);
76-
clearTimeout(this.animationTimeout);
77-
clearTimeout(this.repeatTimeout);
78-
window.removeEventListener("resize", this._calculateContentHeight);
79-
}
80-
81-
_calculateContentHeight = () => {
82-
if (this.content) {
83-
this.setState({
84-
contentHeight: this.content.clientHeight,
85-
});
86-
}
87-
};
76+
</div>
77+
);
78+
}
8879

89-
_handleTransitionEnd = () => {
90-
const { children, repeatDelay } = this.props;
80+
TextRotater.propTypes = {
81+
children: PropTypes.arrayOf(PropTypes.node),
82+
delay: PropTypes.number,
83+
repeatDelay: PropTypes.number,
84+
// Needed to prevent jump when
85+
// rotating between texts of different widths
86+
maxWidth: PropTypes.number,
87+
};
9188

92-
this.setState(
93-
{
94-
currentIndex: (this.state.currentIndex + 1) % Children.count(children),
95-
isAnimating: false,
96-
},
97-
() => {
98-
this.repeatTimeout = setTimeout(() => {
99-
this.setState({ isAnimating: true });
100-
}, repeatDelay);
101-
},
102-
);
103-
};
104-
}
89+
export default memo(TextRotater);

0 commit comments

Comments
 (0)