Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
March 11, 2021 06:25 pm GMT

Popular patterns and anti-patterns with React Hooks

It's been more than 2 years since Hooks API was added to React. Many projects already adopted the new API and there was enough time to see how the new patterns work in production. In this article I am going to walk you through my list of learnings after maintaining a large hooks-based codebase.

Learning #1. All standard rules apply

Hooks require developers to learn new patterns and follow some rules of hooks. This sometimes makes people think that new pattern dismisses all previous good practices. However, hooks are just yet another way of creating reusable building blocks. If you are creating a custom hook, you still need to apply basic software development practices:

  1. Single-responsibility principle. One hook should encapsulate a single piece of functionality. Instead of creating a single super-hook, it is better to split it into multiple smaller and independent ones
  2. Clearly defined API. Similar to normal functions/methods, if a hook takes too many arguments, it is a signal that this hook needs refactoring to be better encapsulated. There were recommendations of avoiding React components having too many props, same for React hooks they also should have minimal number of arguments.
  3. Predictable behavior. The name of a hook should correspond to its functionality, no additional unexpected behaviours.

Even though these recommendations may look very obvious, it is still important to ensure that you follow them when you are creating your custom hooks.

Learning #2. Dealing with hook dependencies.

Several React hooks introduce a concept of "dependencies" list of things which should cause a hook to update. Most often this can be seen in useEffect, but also in useMemo and useCallback. There is a ESLint rule to help you managing an array of dependencies in your code, however this rule can only check the structure of the code and not your intent. Managing hook dependencies is the most tricky concept and requires a lot of attention from a developer. To make your code more readable and maintainable, you could reduce the number of hook dependencies.

Your hooks-based code could become easier with this simple trick. For example, let's consider a custom hook useFocusMove:

function Demo({ options }) {  const [ref, handleKeyDown] = useFocusMove({    isInteractive: (option) => !option.disabled,  });  return (    <ul onKeyDown={handleKeyDown}>      {options.map((option) => (        <Option key={option.id} option={option} />      ))}    </ul>  );}

This custom hook takes a dependency on isInteractive, which can be used inside the hook implementation:

function useFocusMove({ isInteractive }) {  const [activeItem, setActiveItem] = useState();  useEffect(() => {    if (isInteractive(activeItem)) {      focusItem(activeItem);    }    // update focus whenever active item changes  }, [activeItem, isInteractive]);  // ...other implementation details...}

ESLint rule requires isInteractive argument to be added to useEffect dependencies, because the rule does not know where this custom hook is used and if this argument is ever changing or not. However, as a developer, we know that once defined this function always has the same implementation and adding it to the dependencies array only clutters the code. Standard "factory function" pattern comes to the rescue:

function createFocusMove({ isInteractive }) {  return function useFocusMove() {    const [activeItem, setActiveItem] = useState();    useEffect(() => {      if (isInteractive(activeItem)) {        focusItem(activeItem);      }    }, [activeItem]); // no ESLint rule violation here :)    // ...other implementation details...  };}// usageconst useFocusMove = createFocusMove({  isInteractive: (option) => !option.disabled,});function Demo({ options }) {  const [ref, handleKeyDown] = useFocusMove();  // ...other code unchanged...}

The trick here is to separate run-time and develop-time parameters. If something is changing during component lifetime, it is a run-time dependency and goes to the dependencies array. If it is once decided for a component and never changes in runtime, it is a good idea to try factory function pattern and make hooks dependencies management easer.

Learning #3. Refactoring useEffect

useEffect hook us a place for imperative DOM interactions inside your React components. Sometimes they could become very complex and adding dependencies array on top of that makes it more difficult tor read and maintain the code. This could be solved via extracting the imperative DOM logic outside the hook code. For example, consider a hook useTooltipPlacement:

function useTooltipPosition(placement) {  const tooltipRef = useRef();  const triggerRef = useRef();  useEffect(() => {    if (placement === "left") {      const triggerPos = triggerRef.current.getBoundingElementRect();      const tooltipPos = tooltipPos.current.getBoundingElementRect();      Object.assign(tooltipRef.current.style, {        top: triggerPos.top,        left: triggerPos.left - tooltipPos.width,      });    } else {      // ... and so on of other placements ...    }  }, [tooltipRef, triggerRef, placement]);  return [tooltipRef, triggerRef];}

The code inside useEffect is getting very long and hard to follow and track if the hook dependencies are used properly. To make this simper, we could extract the effect content into a separate function:

// here is the pure DOM-related logicfunction applyPlacement(tooltipEl, triggerEl, placement) {  if (placement === "left") {    const triggerPos = tooltipEl.getBoundingElementRect();    const tooltipPos = triggerEl.getBoundingElementRect();    Object.assign(tooltipEl.style, {      top: triggerPos.top,      left: triggerPos.left - tooltipPos.width,    });  } else {    // ... and so on of other placements ...  }}// here is the hook bindingfunction useTooltipPosition(placement) {  const tooltipRef = useRef();  const triggerRef = useRef();  useEffect(() => {    applyPlacement(tooltipRef.current, triggerRef.current, placement);  }, [tooltipRef, triggerRef, placement]);  return [tooltipRef, triggerRef];}

Our hook has become one line long and easy to track the dependencies. As a side bonus we also got a pure DOM implementation of the positioning which could be used and tested outside of React :)

Learning #4. useMemo, useCallback and premature optimisations

useMemo hook documentation says:

You may rely on useMemo as a performance optimisation

For some reason, developers read this part as "you must" instead of "you may" and attempt to memoize everything. This may sound like a good idea on a quick glance, but it appears to be more tricky when it comes to details.

To make benefits from memoization, it is required to use React.memo or PureComponent wrappers to prevent components from unwanted updates. It also needs very fine tuning and validation that there are no properties changing more often than they should. Any single incorrect propety might break all memoization like a house of cards:

This is a good time to recall YAGNI approach and focus memoization efforts only in a few hottest places of your app. In the remaining parts of the code it is not worth adding extra complexity with useMemo/useCallback. You could benefit from writing more simple and readable code using plain functions and apply memoization patterns later when their benefits become more obvious.

Before going the memoization path, I could also recommend you checking the article "Before You memo()", where you can find some alternatives to memoization.

Learning #5. Other React API still exist

If you have a hammer, everything looks like a nail

The introduction of hooks, made some other React patterns obsolete. For example, useContext hook appeared to be more convenient than Consumer component.

However, other React features still exist and should not be forgotten. For example, let's take this hook code:

function useFocusMove() {  const ref = useRef();  useEffect(() => {    function handleKeyDown(event) {      // actual implementation is extracted outside as shown in learning #3 above      moveFocus(ref.current, event.keyCode);    }    ref.current.addEventListener("keydown", handleKeyDown);    return () => ref.current.removeEventListener("keydown", handleKeyDown);  }, []);  return ref;}// usagefunction Demo() {  const ref = useFocusMove();  return <ul ref={ref} />;}

It may look like a proper use-case for hooks, but why couldn't we delegate the actual event subscription to React instead of doing manually? Here is an alternative version:

function useFocusMove() {  const ref = useRef();  function handleKeyDown(event) {    // actual implementation is extracted outside as shown in learning #3 above    moveFocus(ref.current, event.keyCode);  }  return [ref, handleKeyDown];}// usagefunction Demo() {  const [ref, handleKeyDown] = useFocusMove();  return <ul ref={ref} onKeyDown={handleKeyDown} />;}

The new hook implementation is shorter and has an advantage as hook consumers can now decide where to attach the listener, in case if they have more complex UI.

This was only one example, there could be many other scenarios, but the primary point remains the same there are many React patterns (high-order components, render props, and others) which still exist and make sense even if hooks are available.

Conclusion

Basically, all learnings above go to one fundamental aspect: keep the code short and easy to read. You will be able to extend and refactor it later in the future. Follow the standard programming patterns and your hook-based codebase will live long and prosper.


Original Link: https://dev.to/justboris/popular-patterns-and-anti-patterns-with-react-hooks-4da2

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To