5 Tips to Avoid React Hooks

Original text: kentcdodds.com/blog/react-…

In this article, we explore common problems with React Hooks and how to avoid them.

React Hooks were introduced in October 2018 and released in February 2019. Since React Hooks were released, many developers have used Hooks in their projects, because Hooks do make it a lot easier to manage component state and side effects.

There is no doubt that React Hooks are currently a hot topic in the React ecosystem, with more and more developers and open source libraries introducing Hooks. While React Hooks are now popular, their introduction also requires developers to change the way they think about component life cycles, states, and side effects; If you don’t have a good understanding of React Hooks, using them blindly will cause you some unexpected bugs. OK, so let’s take a look at the pitfalls of using Hooks and how we can change the way we think to avoid them.

Problem 1: Trying to use Hooks before you understand them

The official documentation on React Hooks is very detailed, and I strongly recommend that you read through the official documentation before using Hooks, especially the FAQ section, which contains many of the issues and solutions that will be used in actual development. Give yourself an hour or two to read through the official documentation. It will go a long way to understanding Hooks and save you a lot of time in actual development in the future.

In the meantime, I suggest you also check out Sophie, Dan, and Ryan’s share introducing Hooks.

Solution to the first problem: peruse official documentation and FAQ 📚

Problem 2: Not using (or ignoring) the React Hooks ESLint plugin

Eslint-plugin-react-hooks the esLint plugin was released in conjunction with the release of React Hooks. This plug-in contains two verification rules: Rules of hooks and Exhaustive deps. The default recommended configuration is to set Rules of hooks to error and Exhaustive deps to Warning.

I strongly recommend that you install, use, and follow these two rules. Not only does it help you find bugs that might otherwise be missed, it also teaches you about code and hooks in the process, and of course, it provides auto-fix for code. Cool.

In my conversations with many developers, I find that many are confused by this Exhaustive deps rule. Therefore, I wrote a simple demo to show what bugs can result if this rule is ignored.

Suppose we have two pages: one is the dog 🐶 List page, showing a List of dog names; One is the Detail page of a particular dog. On the list page, clicking on a dog’s name opens a page of details for that dog.

OK, on the dog details page, we have a dog details component, DogInfo, which receives the dog’s dogId and retrieves the corresponding details according to the dogId request API:

function DogInfo({dogId}) {
  const [dog, setDog] = useState(null)
  // imagine you also have loading/error states. omitting to save space...
  useEffect((a)= > {
    getDog(dogId).then(d= > setDog(d))
  }, []) / / 😱
  return <div>{/* render the dog info here */}</div>
}
Copy the code

In the above code, our useEffect dependency list is an empty array because we only want to make a request once when the component mounts. So far, the code is fine. Now, suppose our dog details page UI has changed a bit, adding a list of “related dogs.” Click on one of the “related Dogs” lists and our DogInfo component will not be updated to the dog details, even though the DogInfo component has been rerendered.

Now, clicking on an item in the “Related Dogs” list triggers a rerender of the details page and passes the clicked dog dogId to DogInfo, but since we wrote an empty array in DogInfo’s useEffect dependency, Cause the useEffect to not be re-executed.

Well, here’s the modified code:

function DogInfo({dogId}) {
  const [dog, setDog] = useState(null)
  // imagine you also have loading/error states. omitting to save space...
  useEffect((a)= > {
    getDog(dogId).then(d= > setDog(d))
  }, [dogId]) / / ✅
  return <div>{/* render the dog info here */}</div>
}
Copy the code

This leads us to the key conclusion: if a useEffect dependency really never changes, there is no problem adding that dependency to the uesEffect dependency array. Also, if you think a dependency won’t change, but it does, this will help you find bugs in your code.

There are many other scenarios that are more difficult to identify and analyze than this example. For example, if you call a function in useEffect (defined outside of useEffect) but don’t add the function to the dependency, the code is probably buggy. Believe me, every time I ignore this rule, I regret not following it in the first place.

Note that this rule (Exhaustive deps) will sometimes fail to properly analyze problems in your code due to some of the limitations ESLint has in code static analysis. Maybe that’s why it defaults to warning instead of error. When it doesn’t parse your code correctly, it will give you some warning messages. In this case, I suggest you refactor your code a little to make sure it is properly parsed. If the code cannot be correctly analyzed after reconstruction, it may be another way to partially close this rule, in order to continue coding without delay.

Solution to the second problem: ** Install, use, and comply with ESLint **.

Problem 3 :(mistakenly) think of Hooks in terms of the component lifecycle

Before React Hooks came along, we could use built-in component lifecycle methods in class components to tell React when and what it should do:

class LifecycleComponent extends React.Component {
  constructor() {
    // initialize component instance
  }
  componentDidMount() {
    // run this code when the component is first added to the page
  }
  componentDidUpdate(prevProps, prevState) {
    // run this code when the component is updated on the page
  }
  componentWillUnmount() {
    // run this code when the component is removed from the page
  }
  render() {
    // call me anytime you need some react elements...}}Copy the code

Since Hooks were released, there is no problem writing class components like this (nor for the foreseeable future), this way of writing components has been around for years. Hooks provide a number of benefits, one of my favorite is useEffect. Hooks make components more declarative. With Hooks, instead of telling “which part of a component’s life cycle an action should be executed”, we can more intuitively tell React, “When something changes, I want the action to be executed.”

So now our code looks like this:

function HookComponent() {
  React.useEffect((a)= > {
    // This side effect code is here to synchronize the state of the world
    // with the state of this component.
    return function cleanup() {
      // And I need to cleanup the previous side-effect before running a new one
    }
    // So I need this side-effect and it's cleanup to be re-run...
  }, [when, any, ofThese, change])
  React.useEffect((a)= > {
    // this side effect will re-run on every single time this component is
    // re-rendered to make sure that what it does is never stale.
  })
  React.useEffect((a)= > {
    // this side effect can never get stale because
    // it legitimately has no dependencies
  }, [])
  return /* some beautiful react elements */
}
Copy the code

Ryan Florence explained the change in thinking from another Angle.

One of the main reasons I like this feature (useEffect) is that it helps me avoid a lot of bugs. In past component-based development, I’ve found that many times I’ve introduced bugs, I forgot to handle a prop or state change in componentDidUpdate; On the other hand, I handled a prop or state change in componentDidUpdate, but forgot to undo the side effects of the previous change. For example, if you make an HTTP request and some prop or state of the component changes before the HTTP is complete, you should usually cancel the HTTP request.

In the React Hooks scenario, you still need to think about when your side effects will be implemented, but you don’t have to think about what lifecycle the side effects will be implemented in, you have to think about how to keep the results of the side effects synchronized with the state of the component. It takes some effort to understand this, but once you do, you’ll avoid a lot of bugs.

Therefore, the only reason you can set the useEffect dependency to an empty array is because it really doesn’t depend on any external variables, not because you think this side effect only needs to be performed once when the component mounts.

Third problem fix: Don’t think of React Hooks in terms of the component lifecycle, think of them if you keep your side effects consistent with the component state

Problem 4: Worrying too much about performance

Some developers freak out when they see the following code:

function MyComponent() {
  function handleClick() {
    console.log('clicked some other component')}return <SomeOtherComponent onClick={handleClick} />
}
Copy the code

They are usually worried for two reasons:

  1. We are inMyComponentFunctions are defined internallyhandleClickWhich means, every timeMyComponentRender, will redefine a differenthandleClick
  2. Every time WE render, we’ll get a new onehandleClickPassed on toSomeOtherComponentIt means we can’t passReact.memo.React.PureComponentorshouldComponentUpdateTo optimizeSomeOtherComponentThe performance of this will causeSomeOtherComponentA lot of unnecessary rerender

For the first problem, the JavaScript engine (even on low-end phones) defines the execution of a new function very quickly. You will almost never encounter APP performance degradation due to redefining functions.

Second, unnecessary repetition of render does not necessarily cause performance problems. Just because the component is rerendered does not mean that the actual DOM will be modified, usually DOM modification is the slow part. React does a good job of optimizing performance, and often you don’t need to introduce extra work to improve performance.

If this extra repetition of render is causing your APP to be slow, you should first clarify why the repetition of Render is so slow. If a single render itself is slow, causing the APP to freeze with extra render repeats, then even if you avoid extra render repeats, you’re likely still facing performance issues. Once you’ve fixed the cause of slow render, you may find that repeated render doesn’t cause your APP to freeze.

If you’re really sure that the extra render repetition is causing the APP performance issues, then you can use some of the performance optimization apis built into React, such as React.memo, react. useMemo, and react. useCallback. You can read about useMemo and useCallback in this blog post. Note: Sometimes your APP will lag even more after you implement performance optimizations… Therefore, you must compare performance checks before and after performance optimization.

Also keep in mind that the Production version of React performs much better than the Development version.

Fourth problem fix: Remember that React is inherently fast, so don’t worry too much or optimize your performance too soon.

Problem 5: Too much emphasis on Hooks tests

I have noticed that some developers are worried that if they migrate components to React Hooks, they will need to rewrite all the corresponding test code. Depending on how your test code is implemented, this may or may not be a valid concern.

Use React Hooks to test code. If your test code looks like this:

test('setOpenIndex sets the open index state properly', () = > {// using enzyme
  const wrapper = mount(<Accordion items={} [] />)
  expect(wrapper.state('openIndex')).toBe(0)
  wrapper.instance().setOpenIndex(1)
  expect(wrapper.state('openIndex')).toBe(1)
})
Copy the code

If this is the case, then you are taking the opportunity to re-write the test code to optimize it. Without a doubt, you should discard code like this and replace it with something like this:

test('can open accordion items to see the contents', () = > {const hats = {title: 'Favorite Hats'.contents: 'Fedoras are classy'}
  const footware = {
    title: 'Favorite Footware'.contents: 'Flipflops are the best',}const items = [hats, footware]
  // using React Testing Library
  const {getByText, queryByText} = render(<Accordion items={items} />)
  expect(getByText(hats.contents)).toBeInTheDocument()
  expect(queryByText(footware.contents)).toBeNull()
  fireEvent.click(getByText(footware.title))
  expect(getByText(footware.contents)).toBeInTheDocument()
  expect(queryByText(hats.contents)).toBeNull()
})
Copy the code

The key difference between the two tests is that while the old code was testing the concrete implementation of the component, the new test is not. Whether the component is implemented in a class-based manner or in an Hooks manner is the implementation details inside the component. Therefore, if your test code is putting implementation details of the component under test (such as.state() or.instance()), refactoring the component to Hooks does indeed break your test code.

But the developer using your component doesn’t care if your component is implemented based on classes or Hooks. They only care about your component implementing the business logic correctly, or what your component renders to the screen. So if your test code checks what components render to the screen, it doesn’t matter whether your component is implemented based on classes or Hooks.

You can learn more about testing in these two articles: Testing for implementation details and Avoid the Test User.

OK, the solution to this problem is to avoid testing the implementation details of components.

conclusion

Having said that, it all boils down to these tips that will help you avoid the common Hooks problems:

  1. Read the official Hooks documentation carefully, as well as the FAQ section
  2. Install, use, and comply with the esLint plugin-react-hooks plugin
  3. Forget the component lifecycle way of thinking. Correct posture: How to keep side effects and component states in sync.
  4. React itself is fast to implement, so be sure to do some research on it before tuning it too early
  5. Instead of testing the implementation details of a component, focus on the input and output of the component