I recently refactored my project using the hooks API, which is currently in the news, and I have a few bugs to share here.

Consider a scenario where the content of a web page is divided into several pages (each page is fixed), the mouse wheel is triggered to turn the page, and we use the state currPage to record the current page number, and then attach the wheel event to componentDidMount. Class implementation code is as follows:

Class App extends react.ponent {state = {currentPage: 0,} /** @description Specifies the instance private field */ flipSwitch =true;
  filpPage = (event) => {
    if (this.flipSwitch) {
      if(event.deltaY > 0){
        this.setState({currentPage: (this.state.currentPage + 1) % 3});
      }
      if(event.deltaY < 0){
        if(this.state.currentPage > 0){
          this.setState({currentPage: this.state.currentPage - 1});
        }
      }
      this.flipSwitch = false;
      setTimeout(() => {this.flipSwitch = true;}, 1000);
    }
  }

  componentDidMount(){
    window.addEventListener('wheel', this.filpPage);
  }

  componentWillUnmount(){
    window.removeEventListener('wheel', this.filpPage);
  }
  
  render(){
    const classNames = ['unvisible'.'unvisible'.'unvisible'];
    classNames[this.state.currentPage] = 'currPage';

    return (
      <div>
        <CoverPage className={classNames[0]} />
        <ProjectPage className={classNames[1]} />
        <SkillsPage className={classNames[2]} />
      </div>
    )
  }

}
Copy the code

Two hooks are used in this reconstruction: useState and useEffect. I’m sure you’ve all heard of useState, so let me talk about useEffect: First of all, it is on the underlying class in each life cycle, receiving the first argument is a callback function effectCallback, this callback is such a form () = > (void | (() = > void | Undefined), if useEffect does not have a second argument,effectCallback will be called after every render (or componentDidupdate) EctCallback usually also returns a function that acts like componentWillUnmount and is run before the next render or destruction of the component. So here I just need to ‘run component once after the first render instead of every render like componentDidMount’ how do I do that? UseEffect is a second parameter that needs to be passed to useEffect, which is an array. Its role is similar to that of componentShouldUpdate. Normally it is made up of data in the props and state All congruent, if all congruent the effectCallback would not be queued and run, and to achieve the effect that componentDidmount does, you just pass an empty array, and the array is all congruent every time.

And then there’s the question: where’s the flipSwitch variable for function stabilization? The answer is useRef. It’s named ref, It is used to collect DOM elements in render, but the official documentation states that it does more than that :**However, UseRef () is useful for more than the ref attribute. It’s handy for keeping any mutable value around similar to how you’d Use instance fields in classes.** It is not much different from useState, except that it does not return the value directly to you. Instead, it returns the value as {current:value}. here is code:

function App () {
  const [currPage, setCurrPage] = useState(0)
  const classNames = Array(pageCount).fill('unvisible');
  const flipSwitch = useRef(true)
  classNames[currPage] = 'currPage';
  useEffect(() => {
    console.log('add listenner! ')
    const wheelListener = event => {
      if (flipSwitch.current) {
        if(event.deltaY > 0){
          setCurrPage((currPage + 1) % pageCount);
        }
        if(event.deltaY < 0){
          if(currPage > 0){
            setCurrPage(currPage - 1)
          }
        }
        flipSwitch.current = false;
        setTimeout(() => {flipSwitch.current = true;}, 1000);
      }
    };
    window.addEventListener('wheel', wheelListener); // Run before the next update listenerreturnThe function ofreturn () => {console.log('rm listenner! '),window.removeEventListener('wheel', wheelListener)}
  },[]);
  return (
    <div>
      <CoverPage className={classNames[0]} />
      <ProjectPage className={classNames[1]} projects={projects} />
      <SkillsPage className={classNames[2]} skills={skills} />
    </div>
  )
}
Copy the code

Here’s the thing. As soon as the program runs, you’ll find that you can only turn from page 1 to page 2. Ok, let’s see what went wrong. WheelListener fires every time, so the problem is probably the currPage variable. Sure enough, by printing currPage, we see that wheelListener fires the same value of 0 every time.

It turns out that all variables except event in wheelListener were obtained by closure when the function was declared in the first render, so naturally the values except event are always the values in the first render.

Knowing why, there are two possible solutions:

  1. Try to get the immediate value of currPage in wheelListener, because without the this reference in the class syntax this seems to be a dead end
  2. Update wheelListener after each render and delete the old one.

This also solves one of my earlier doubts :’ Isn’t it wasteful to declare a new callback to useEffect every render? ‘, it seems likely that this is the intended use of the official API design, saving the useEffect second parameter.

So, remove the second parameter of useEffect, or set it to [currPage] to fit the usage scenario, and it now works:

function App () {
  const [currPage, setCurrPage] = useState(0)
  const classNames = Array(3).fill('unvisible');
  const flipSwitch = useRef(true)
  classNames[currPage] = 'currPage';
  useEffect(() => {
    console.log('add listenner! ')
    const wheelListener = event => {
      if (flipSwitch.current) {
        if(event.deltaY > 0){
          setCurrPage((currPage + 1) % 3);
        }
        if(event.deltaY < 0){
          if(currPage > 0){
            setCurrPage(currPage - 1)
          }
        }
        flipSwitch.current = false;
        setTimeout(() => {flipSwitch.current = true;}, 1000);
      }
    };
    window.addEventListener('wheel', wheelListener); // Run before the next update callbackreturnThe function ofreturn () => {console.log('rm listenner! '),window.removeEventListener('wheel', wheelListener)}
  }, [currPage]);
  return (
    <div>
      <CoverPage className={classNames[0]} />
      <ProjectPage className={classNames[1]} />
      <SkillsPage className={classNames[2]} />
    </div>
  )
}
Copy the code

The PS: hooks API still smells good:

  1. Functional components look much cleaner.
  2. In contrast to the render- Vm-state class, the hooks API changes the render-states to a simpler version. The state,react also does the work for you, you just need to give it the initial value.
  3. Custom hooks reuse logic that is difficult to reuse under class syntax (generally because this refers to a problem).
  4. Functional components combine functional programming with something more imaginative (incorporating compose, currification, etc.).