Requirements: We now have a button to get a captcha that needs to be disabled after clicking, and a 60 second countdown is displayed on the button before a second click. This article discusses the evolution of logic reuse in React through eight implementations of this requirement
Code examples are placed on codesandBox.
Option one uses setInterval
import React from 'react'
export default class LoadingButtonInterval extends React.Component {
state = {
loading: false.btnText: 'Get captcha'.totalSecond: 10
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = (a)= > {
clearInterval(this.timer)
this.setState({
loading: false.totalSecond: 10
})
}
setTime = (a)= > {
this.timer = setInterval((a)= > {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState((a)= > ({
totalSecond: totalSecond - 1}})),1000)
}
onFetch = (a)= > {
this.setState((a)= > ({ loading: true }))
const { totalSecond } = this.state
this.setState((a)= > ({
totalSecond: totalSecond - 1
}))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>)}}Copy the code
Scheme 2 uses setTimeout
import React from 'react'
export default class LoadingButton extends React.Component {
state = {
loading: false.btnText: 'Get captcha'.totalSecond: 60
}
timer = null
componentWillUnmount() {
this.clear()
}
clear = (a)= > {
clearTimeout(this.timer)
this.setState({
loading: false.totalSecond: 60
})
}
setTime = (a)= > {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout((a)= > {
this.setTime()
}, 1000)
}
onFetch = (a)= > {
this.setState((a)= > ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<button disabled={loading} onClick={this.onFetch}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>)}}Copy the code
We may soon write two such components. It doesn’t make much difference if you use setTimeout or setInterval. But I would recommend setTimeout because everything is recursive.
But there’s more to it than that. You can see the captcha we just got. If another page has the same requirements, the component can only be copied completely again. This can’t be appropriate.
So what do we do?
Parameter of scheme 3 is extracted to Props 1
import React from "react";
class LoadingButtonProps extends React.Component {
constructor(props) {
super(props);
this.initState = {
loading: false.btnText: this.props.btnText || "Obtain captcha code".totalSecond: this.props.totalSecond || 60
};
this.state = { ... this.initState }; } timer =null;
componentWillUnmount() {
this.clear();
}
clear = (a)= > {
clearTimeout(this.timer);
this.setState({ ... this.initState }); }; setTime =(a)= > {
const { totalSecond } = this.state;
if (totalSecond <= 0) {
this.clear();
return;
}
this.setState({
totalSecond: totalSecond - 1
});
this.timer = setTimeout((a)= > {
this.setTime();
}, 1000);
};
onFetch = (a)= > {
const { loading } = this.state;
if (loading) return;
this.setState((a)= > ({ loading: true }));
this.setTime();
};
render() {
const { loading, btnText, totalSecond } = this.state;
return (
<button disabled={loading} onClick={this.onFetch}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>); }}class LoadingButtonProps1 extends React.Component {
render() {
return<LoadingButtonProps btnText={" Get verification code 1"} totalSecond={10} />; }} class LoadingButtonProps extends React.Component {render() {return <LoadingButtonProps btnText={" LoadingButtonProps2 "} totalSecond={20} />; } } export default () => ( <div> <LoadingButtonProps1 /> <LoadingButtonProps2 /> </div> );Copy the code
For this requirement, I want to prop up a common parent component. It seems pretty nice when you think about it.
Then the requirements change:
First point: The API for obtaining captcha is different in the two places. Second point: I need to do something else before I can get the captcha
I scratched my head. What should I do?
Parameter of scheme 4 is extracted to Props 2
import React from 'react'
class LoadingButtonProps extends React.Component {
// static defaultProps = {
// loading: false,
// btnText: 'get verification code ',
// totalSecond: 10,
// onStart: () => {},
// onTimeChange: () => {},
// onReset: () => {}
// }
timer = null
componentWillUnmount() {
this.clear()
}
clear = (a)= > {
clearTimeout(this.timer)
this.props.onReset()
}
setTime = (a)= > {
const { totalSecond } = this.props
console.error(totalSecond)
if (this.props.totalSecond <= 0) {
this.clear()
return
}
this.props.onTimeChange()
this.timer = setTimeout((a)= > {
this.setTime()
}, 1000)
}
onFetch = (a)= > {
if (this.loading) return
this.setTime()
this.props.onStart()
}
render() {
return <div onClick={this.onFetch}>{this.props.children}</div>}}class LoadingButtonProps1 extends React.Component {
totalSecond = 10
state = {
loading: false.btnText: 'Get captcha 1'.totalSecond: this.totalSecond
}
onTimeChange = (a)= > {
const { totalSecond } = this.state
this.setState((a)= > ({ totalSecond: totalSecond - 1 }))
}
onReset = (a)= > {
this.setState({
loading: false.totalSecond: this.totalSecond
})
}
onStart = (a)= > {
this.setState((a)= > ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>
</LoadingButtonProps>)}}class LoadingButtonProps2 extends React.Component {
totalSecond = 15
state = {
loading: false.btnText: 'Obtain captcha 2'.totalSecond: this.totalSecond
}
onTimeChange = (a)= > {
const { totalSecond } = this.state
this.setState((a)= > ({ totalSecond: totalSecond - 1 }))
}
onReset = (a)= > {
this.setState({
loading: false.totalSecond: this.totalSecond
})
}
onStart = (a)= > {
this.setState((a)= > ({ loading: true }))
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<LoadingButtonProps
loading={loading}
totalSecond={totalSecond}
onStart={this.onStart}
onTimeChange={this.onTimeChange}
onReset={this.onReset}
>
<button disabled={loading}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>
</LoadingButtonProps>)}}export default() = > (<div>
<LoadingButtonProps1 />
<LoadingButtonProps2 />
</div>
)
Copy the code
Huh? And so on. So this operation only shares the recursive reduction of time, right? It seems that there is a lot of duplicate code, and it doesn’t feel much different from the old version.
So what do we do?
Plan five: Try HOC
import React from 'react'
function loadingButtonHoc(WrappedComponent, initState) {
return class extends React.Component {
constructor(props) {
super(props)
this.initState = initState || {
loading: false.btnText: 'Get captcha'.totalSecond: 60
}
this.state = { ... this.initState } } timer =null
componentWillUnmount() {
this.clear()
}
clear = (a)= > {
clearTimeout(this.timer)
this.setState({ ... this.initState }) } setTime =(a)= > {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout((a)= > {
this.setTime()
}, 1000)
}
onFetch = (a)= > {
const { loading } = this.state
if (loading) return
this.setState((a)= > ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return (
<WrappedComponent
{. this.props}
onClick={this.onFetch}
loading={loading}
btnText={btnText}
totalSecond={totalSecond}
/>
)
}
}
}
class LoadingButtonHocComponent extends React.Component {
render() {
const { loading, btnText, totalSecond, onClick } = this.props
return (
<button disabled={loading} onClick={onClick}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>) } } const LoadingButtonHocComponent1 = loadingButtonHoc(LoadingButtonHocComponent, { loading: false, btnText: 'Get capTcha Hoc1', totalSecond: 20 }) const LoadingButtonHocComponent2 = loadingButtonHoc(LoadingButtonHocComponent, { loading: false, btnText: Hoc2, totalSecond: 12}) export default () => (<div>
<LoadingButtonHocComponent1 />
<LoadingButtonHocComponent2 />
</div>
)
Copy the code
We rewrote the entire logic with higher-order components. Looks like you’ve basically met your needs? So the idea here is that if you expose the onClick or onStart event, it’s up to the external component to decide when to execute it, so you can do it anyway, right
Plan 6 renderProps
import React from 'react'
class LoadingButtonRenderProps extends React.Component {
constructor(props) {
super(props)
this.initState = {
loading: false.btnText: this.props.btnText || 'Get captcha'.totalSecond: this.props.totalSecond || 60
}
this.state = { ... this.initState } } timer =null
componentWillUnmount() {
this.clear()
}
clear = (a)= > {
clearTimeout(this.timer)
this.setState({ ... this.initState }) } setTime =(a)= > {
const { totalSecond } = this.state
if (totalSecond <= 0) {
this.clear()
return
}
this.setState({
totalSecond: totalSecond - 1
})
this.timer = setTimeout((a)= > {
this.setTime()
}, 1000)
}
onFetch = (a)= > {
const { loading } = this.state
if (loading) return
this.setState((a)= > ({ loading: true }))
this.setTime()
}
render() {
const { loading, btnText, totalSecond } = this.state
return this.props.children({
onClick: this.onFetch,
loading: loading,
btnText: btnText,
totalSecond: totalSecond
})
}
}
class LoadingButtonRenderProps1 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'Get the verification codeRP1'} totalSecond={15}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>
)}
</LoadingButtonRenderProps>)}}class LoadingButtonRenderProps2 extends React.Component {
render() {
return (
<LoadingButtonRenderProps btnText={'Get the verification codeRP1'} totalSecond={8}>
{({ loading, btnText, totalSecond, onClick }) => (
<button disabled={loading} onClick={onClick}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>
)}
</LoadingButtonRenderProps>)}}export default() = > (<div>
<LoadingButtonRenderProps1 />
<LoadingButtonRenderProps2 />
</div>
)
Copy the code
Hey, we used render Props to rewrite the functionality implemented on Hoc. Personally, it’s simpler and more elegant than Hoc!
React Hooks
import React, { useState, useEffect, useRef, useCallback } from 'react'
function LoadingButtonHooks(props) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(props.loading)
const [btnText, setBtnText] = useState(props.btnText)
const [totalSecond, setTotalSecond] = useState(props.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback((a)= > {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(props.totalSecond)
countRef.current = props.totalSecond
})
const setTime = useCallback((a)= > {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)
timeRef.current = setTimeout((a)= > {
setTime()
}, 1000)})const onStart = useCallback((a)= > {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})
useEffect((a)= > {
return (a)= > {
clearTimeout(timeRef.current)
}
}, [])
return (
<button disabled={loading} onClick={onStart}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>
)
}
LoadingButtonHooks.defaultProps = {
loading: false.btnText: 'Get captcha'.totalSecond: 10
}
export default() => (<div> <LoadingButtonHooks loading={false} btnText={' Get verification code hooks1'} totalSecond={10} /> <LoadingButtonHooks loading={false} btnText={' Get verification code hooks1'} totalSecond={10} / Loading ={false} btnText={' hooks2'} totalSecond={11} /> </div>)Copy the code
We rewrote the entire program using hooks, which allowed us to separate UI and state more explicitly, and also addressed some JSX nesting hell issues with renderProps at multiple levels. It certainly feels as if Hooks are not that different from renderProps versions in this example.
Eight uesHooks scheme
import React, { useState, useEffect, useRef, useCallback } from 'react'
function useLoadingTimer(initState) {
const timeRef = useRef(null)
const [loading, setLoading] = useState(initState.loading)
const [btnText, setBtnText] = useState(initState.btnText)
const [totalSecond, setTotalSecond] = useState(initState.totalSecond)
const countRef = useRef(totalSecond)
const clear = useCallback((a)= > {
clearTimeout(timeRef.current)
setLoading(false)
setTotalSecond(initState.totalSecond)
countRef.current = initState.totalSecond
})
const setTime = useCallback((a)= > {
if (countRef.current <= 0) {
clear()
return
}
countRef.current = countRef.current - 1
setTotalSecond(countRef.current)
timeRef.current = setTimeout((a)= > {
setTime()
}, 1000)})const onStart = useCallback((a)= > {
if (loading) return
countRef.current = totalSecond
setLoading(true)
setTime()
})
useEffect((a)= > {
return (a)= > {
clearTimeout(timeRef.current)
}
}, [])
return {
onStart,
loading,
totalSecond,
btnText
}
}
const LoadingButtonHooks1 = (a)= > {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false.btnText: 'Obtain verification code UseHooks1'.totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>)}const LoadingButtonHooks2 = (a)= > {
const { onStart, loading, totalSecond, btnText } = useLoadingTimer({
loading: false.btnText: 'Obtain verification code UseHooks2'.totalSecond: 10
})
return (
<button disabled={loading} onClick={onStart}>{! loading ? BtnText: 'Please wait ${totalSecond} seconds.. `}</button>)}export default() = > (<div>
<LoadingButtonHooks1 />
<LoadingButtonHooks2 />
</div>
)
Copy the code
Of course, it is more decouple to extract the hooks completely separately as useHooks, and then write a component to combine uesHooks.
In the example above, we used 8 different scenarios in React to describe the process of writing the same function. There is a bit of “back” word of many ways of writing meaning. However, it also represents the process of change in the minds of the React community on the choice of implementation. I think one solution is definitely better than the other. There was also a lot of discussion in the community about HOC vs renderProps.
I just hope you can look at this process dialectically, and I hope it will bring you some new ideas when writing the React component.
Reference links:
- React Chinese official website Hook introduction
- awesome-react-hooks