DEV Community

Ekaterina Anishkina
Ekaterina Anishkina

Posted on • Edited on

Yet another React Dos and Don'ts

It’s another one of the React Dos and Don'ts which you have found a lot on the internet. But give me a chance and keep reading. I’ve been developing React web applications for about 8 years. For the last 3 years I’ve been working on optimizing React apps to make them fast with good FPS. Let’s get started.
The React team has created really good and detailed documentation. I like it much more than the previous one. My favorite part is about the Render and Commit phases. Please read it if you haven’t. The most common mistake that I’ve seen during code reviews was triggering a redundant commit phase. It’s important to batch state updates and avoid triggering setState synchronously in effect.

Don’t

const ModalWithState = ({ isVisible }) => {
  const [isModalContentVisible, setIsModalContentVisible] = useState(isVisible);

  useEffect(() => {
    setIsModalContentVisible(isVisible);
  }, [isVisible]);

  return (
    <Modal
      isModalContentVisible={isModalContentVisible}
      onShow={() => {
        setIsModalContentVisible(true);
      }}
      onClose={() => {
        setIsModalContentVisible(false);
      }}
    >
      <div>Modal content</div>
    </Modal>
  );
};
Enter fullscreen mode Exit fullscreen mode

Do

const ModalWithState = ({ isVisible }) => {
  const [isModalContentVisible, setIsModalContentVisible] = useState(isVisible);
  const [prevIsVisible, setIsPrevIsVisible] = useState(isVisible);

  if (isVisible !== prevIsVisible) {
    setIsModalContentVisible(isVisible);
    setIsPrevIsVisible(isVisible);
  }

  return (
    <Modal
      isModalContentVisible={isModalContentVisible}
      onShow={() => {
        setIsModalContentVisible(true);
      }}
      onClose={() => {
        setIsModalContentVisible(false);
      }}
    >
      <div>Modal content</div>
    </Modal>
  );
};
Enter fullscreen mode Exit fullscreen mode

There are some exceptions when it’s necessary to update state synchronously. E.g. when handling different client and server content.
And keep in mind - “Always check whether you can reset all state with a key or calculate everything during rendering instead”.

The next part is a long discussion about when to use useCallback/useMemo and when not to.
There are two opposite approaches:

  1. Use it only when you’ve identified performance bottlenecks. There is a good article about this called - When to useMemo and useCallback.
  2. You can “put down a pillow” and use it everywhere. I have run tests and found that it has a negative impact on performance only with high numbers of useMemo/useCallback usage on the page (from ~2000 on weaker devices).

I recommend striking a happy medium.

  1. If you never benefit from that, don’s use it:
      1.1. Don’t use useCallback when you pass event handlers as props in the base components (like button, div, etc.)
      1.2. Don’t use useMemo for primitive memoization.
      1.3. Free memoization is always the best one to use.

  2. If you definitely benefit from that now or in the future:
      2.1. Apply useCallback if you pass event handlers in the memoized components.
      2.2. Don’t pass new functions or objects via context for every render unintentionally.
      2.3. Use useCallback/useMemo when you don’t want to trigger effects for every render.
      2.4. Use useCallback for non-base components.

1.1. Don’t use useCallback when you pass event handlers as props in the base components (like button, div, etc.)

In the example below <button> is a base component. If you use css-in-js please keep in mind that the styled button is also actually the base component.

Don’t

const Parent = ({ value }) => {
    const [v, setV] = useState(0);

    const onClick = useCallback(() => setV(v => v + 1), []);

    return <button onClick={onClick}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Do

const Parent = ({ value }) => {
    const [v, setV] = useState(0);

    return <button onClick={() => setV(v => v + 1)} />;
}
Enter fullscreen mode Exit fullscreen mode

1.2. Don’t use useMemo for primitive memoization.

A huge number of useMemo usage (2K - 100K, depending on device) could increase render time by 50%. I believe that ~2K useMemo is a rare case for production applications, but why do you use it if there is no sense.

Don’t

const MyComponent = ({ value }) => {
    const text = useMemo(() => `My value is ${value}`, [value]);

    return <div>{text}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Do

const MyComponent = ({ value }) => {
    const text = `My value is ${value}`;

    return <div>{text}</div>;
}
Enter fullscreen mode Exit fullscreen mode

1.3. Free memoization is always the best one to use.

I hope you have a good habit of moving initializations of constants outside of the render. In a very artificial example with a huge amount of re-renders there is a render degradation up to 60%.

Don’t

const MyComponent = () => {
    const list = useMemo(() => ['one', 'two', 'three', 'four'], []);

    return <ChildMemo list={list} />;
}
Enter fullscreen mode Exit fullscreen mode

Do

const LIST = ['one', 'two', 'three', 'four'];

const MyComponent = () => {
    return <ChildMemo list={LIST} />;
}
Enter fullscreen mode Exit fullscreen mode

The cases below if you need it now or it could be applicable in the future.

2.1. Apply useCallback if you pass event handlers in the memoized components.

Don’t

const ChildMemo = memo(function Child() {
    return  <button onClick={onClick}>Click me</button>;
});

const Parent = () => {
    const [v, setV] = useState(0);

    return <ChildMemo onClick={() => setV(v => v + 1)} />;
}
Enter fullscreen mode Exit fullscreen mode

Do

const Parent = () => {
    const [v, setV] = useState(0);

    const handleOnClick = useCallback(() => setV(v => v + 1), []);

    return <ChildMemo onClick={handleOnClick} />;
}
Enter fullscreen mode Exit fullscreen mode

2.2. Don’t pass new functions or objects via context for every render unintentionally.

It avoids redundant re-renders of context subscribed components. You can find details in the documentation - Optimizing re-renders when passing objects and functions. I recommend enabling the eslint rule regarding that - react/jsx-no-constructed-context-values.

Don’t

return (
    <SomeContext.Provider value={{ foo: 'bar' }}>
        ...
    </SomeContext.Provider>
)
Enter fullscreen mode Exit fullscreen mode

Do

const foo = useMemo(() => ({ foo: 'bar' }), []);
return (
    <SomeContext.Provider value={foo}>
        ...
    </SomeContext.Provider>
)
Enter fullscreen mode Exit fullscreen mode

2.3. Always use useCallback if you don’t want to trigger effects for every render.

Also it gets rid of the disabling eslint rule // eslint-disable-next-line react-hooks/exhaustive-deps.

Don’t

const Child = ({ handleData, data }) => {
    useEffect(() => {
        myFetch().then((res) => {
            handleData(res);
        })
    // eslint-disable-next-line react-hooks/exhaustive-deps
    }, [])

    return <div>{data}</div>
};

const Parent = () => {
    const [data, setData] = useState(null);

    return <Child handleData={(data) => setData(data)} data={data} />;
}
Enter fullscreen mode Exit fullscreen mode

Do

const Child = ({ handleData, data }) => {
    useEffect(() => {
        myFetch().then((res) => {
            handleData(res);
        })
    }, [handleData])

    return <div>{data}</div>
};

const Parent = () => {
    const [data, setData] = useState(null);

    const handleData = useCallback((data) => setData(data), []);

    return <Child handleData={handleData} data={data} />;
}
Enter fullscreen mode Exit fullscreen mode

2.4. Call useCallback for non base components.

Because in the future these components can be transformed to memoised components and such event handlers can break memoization.

Don’t

const Parent = ({ value }) => {
    const [v, setV] = useState(0);

    return <MyButton onClick={() => setV(v => v + 1)}>Click me</button>;
}
Enter fullscreen mode Exit fullscreen mode

Do

const Parent = ({ value }) => {
    const [v, setV] = useState(0);

    const handleOnClick = useCallback(() => setV(v => v + 1), []);

    return <MyButton onClick={handleOnClick} />;
}
Enter fullscreen mode Exit fullscreen mode

Don’t dynamically create a new component on every render.

React will throw away the previous DOM subtree and create it from scratch instead of re-calculating the difference.
It negatively affects performance and can lead unpredictable behaviour by unmounting and mounting such components on every re-render:

  1. All effects will be triggered.
  2. The state will not be saved.
  3. Memoization will be broken.

Don’t

const Parent = ({ value }) => {
    const Child = () => <div>{value}</div>;

    return <Child />;
}
Enter fullscreen mode Exit fullscreen mode

Do

const Child = ({ value }) => <div>{value}</div>;

const Parent = ({ value }) => {
    return <Child value={value} />;
}
Enter fullscreen mode Exit fullscreen mode

Don’t break memoization.

When you use memo don’t pass children as props. In most cases it triggers re-render of memoized components for every parents’ re-renders. An exception is if children is a primitive data type (string, number, etc.).

Don’t

const Child = () => {
    return <div>child</div>;
};

const MemoComponent = memo(function MemoComponent({ children }) {
    // some heavy render

    return <div>{children}</div>;
});

const Parent = () => {
    return (
        <MemoComponent>
            <Child />
        </MemoComponent>
    );
};
Enter fullscreen mode Exit fullscreen mode

Do1 (better than Do2)

const Child = () => {
    return <div>child</div>;
};

const MemoComponent = memo(function MemoComponent() {
    // some heavy render

    return (
        <div>
            <Child />
        </div>
    );
});

const Parent = () => {
    return <MemoComponent />;
};

Enter fullscreen mode Exit fullscreen mode

Do2

const Child = () => {
    return <div>child</div>;
};

const MemoComponent = memo(function MemoComponent({ children }) {
    // some heavy render

    return <div>{children}</div>;
});

const Parent = () => {
    const memoChild = useMemo(() => <Child />, [])

    return (
        <MemoComponent>
            {memoChild}
        </MemoComponent>
    );
};

Enter fullscreen mode Exit fullscreen mode

useReducer is often a better option.

Sometimes developers prefer to use a lot of useState instead of one useReducer. And I failed to find the valid reasons for that. Since we have automatic batching in React 18 this leads to less performance bottlenecks but it has still been a bad option. In the very artificial example with 11 useState instead of 1 useReducer there is a 45% re-render time growth.

Don’t

const MyComponent = ({ id }) => {
    const [data, setData] = useState(null);
    const [isLoading, setIsLoading] = useState(true);
    const [error, setError] = useState(false);

    useEffect(() => {
        setIsLoading(true);
        myFetch(id).then((data) => {
            setData(data);
            setError(null);
        }).catch((e) => {
            setError(e);
        }).finally(() => {
            setIsLoading(false);
        })

    }, [id])

    if (isLoading) {
        return 'Loading';
    }

    if (error) {
        return 'Error';
    }

    return <div>{data}</div>;
}
Enter fullscreen mode Exit fullscreen mode

Do

const reducer = (state, action) => {
    switch (action.type) {
        case 'set data': {
            return {
                ...state,
                isLoading: false,
                error: null,
                data: action.payload,
            }
        }
        case 'set isLoading': {
            return {
                ...state,
                isLoading: action.payload,
            }
        }
        case 'set error': {
            return {
                ...state,
                error: action.payload,
            }
        }
        default: {
            throw Error('Unknown action: ' + action.type);
        }
    }
};

const MyComponent = ({ id }) => {
    const [{ data, isLoading, error }, dispatch] = useReducer(reducer, {
        data: null,
        isLoading: true,
        error: null,
    });

    useEffect(() => {
        dispatch({ type: 'set isLoading', payload: true });
        myFetch(id).then((data) => {
            dispatch({ type: 'set data', payload: data });
        }).catch((e) => {
            dispatch({ type: 'set error', payload: e });
        })
    }, [id])

    if (isLoading) {
        return 'Loading';
    }

    if (error) {
        return 'Error';
    }

    return <div>{data}</div>;
}
Enter fullscreen mode Exit fullscreen mode

useState lazy initialization helps you avoid expensive calculations on every render.

There is documentation.

Don’t

const MyComponent = (props) => {
    const [value, setValue] = useState(calculateSomethingExpensive(props));

    return <div>{value}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Do

const MyComponent = (props) => {
    const [value, setValue] = useState(() => calculateSomethingExpensive(props));

    return <div>{value}</button>;
}
Enter fullscreen mode Exit fullscreen mode

Keep you render pure.

Otherwise it could lead to different bugs such as unpredictable ref values.

Don’t

const MyComponent = (props) => {
    const ref = useRef(props);
    ref.current = props;

    return <div>text</div>;
}
Enter fullscreen mode Exit fullscreen mode

Do

const MyComponent = (props) => {
    const ref = useRef(props);

    useEffect(() => {
        ref.current = props;
    })

    return <div>text</div>;
}
Enter fullscreen mode Exit fullscreen mode

I hope you’ve found something useful for yourself. Happy coding!

Top comments (0)