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