Yet another React Dos and Don'ts
Tags: react, webdev, javascript, tutorial
Published: 2023-11-08 07:57:03

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](https://react.dev/). I like it much more than [the previous one](https://legacy.reactjs.org/docs/getting-started.html). My favorite part is about the [Render and Commit phases](https://react.dev/learn/render-and-commit). 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**
```jsx
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>
  );
};
```

**Do**
```jsx
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>
  );
};
```

There are some exceptions when it’s necessary to update state synchronously. E.g. when handling [different client and server content](https://react.dev/reference/react-dom/hydrate#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”](https://react.dev/learn/you-might-not-need-an-effect#adjusting-some-state-when-a-prop-changes).

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](https://kentcdodds.com/blog/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**
```jsx
const Parent = ({ value }) => {
    const [v, setV] = useState(0);

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

    return <button onClick={onClick}>Click me</button>;
}
```

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

    return <button onClick={() => setV(v => v + 1)} />;
}
```

#### 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**
```jsx
const MyComponent = ({ value }) => {
    const text = useMemo(() => `My value is ${value}`, [value]);

    return <div>{text}</div>;
}
```

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

    return <div>{text}</div>;
}
```


#### 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**
```jsx
const MyComponent = () => {
    const list = useMemo(() => ['one', 'two', 'three', 'four'], []);

    return <ChildMemo list={list} />;
}
```

**Do**
```jsx
const LIST = ['one', 'two', 'three', 'four'];

const MyComponent = () => {
    return <ChildMemo list={LIST} />;
}
```


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**
```jsx
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)} />;
}
```

**Do**
```jsx
const Parent = () => {
    const [v, setV] = useState(0);

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

    return <ChildMemo onClick={handleOnClick} />;
}
```

#### 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](https://react.dev/reference/react/useContext#optimizing-re-renders-when-passing-objects-and-functions). I recommend enabling the `eslint` rule regarding that - [react/jsx-no-constructed-context-values](https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-no-constructed-context-values.md).

**Don’t**
```jsx
return (
    <SomeContext.Provider value={{ foo: 'bar' }}>
        ...
    </SomeContext.Provider>
)
```

**Do**
```jsx
const foo = useMemo(() => ({ foo: 'bar' }), []);
return (
    <SomeContext.Provider value={foo}>
        ...
    </SomeContext.Provider>
)
```

#### 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**
```jsx
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} />;
}
```

**Do**
```jsx
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} />;
}
```

#### 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**
```jsx
const Parent = ({ value }) => {
    const [v, setV] = useState(0);

    return <MyButton onClick={() => setV(v => v + 1)}>Click me</button>;
}
```

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

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

    return <MyButton onClick={handleOnClick} />;
}
```

#### 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**
```jsx
const Parent = ({ value }) => {
    const Child = () => <div>{value}</div>;

    return <Child />;
}
```

**Do**
```jsx
const Child = ({ value }) => <div>{value}</div>;

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


#### 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**
```jsx
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>
    );
};
```

**Do1 (better than Do2)**
```jsx
const Child = () => {
    return <div>child</div>;
};

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

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

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

```

**Do2**
```jsx
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>
    );
};

```

#### `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](https://react.dev/blog/2022/03/29/react-v18#new-feature-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**
```jsx
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>;
}
```

**Do**
```jsx
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>;
}
```

#### `useState` lazy initialization helps you avoid expensive calculations on every render. 
There is [documentation](https://react.dev/reference/react/useState#avoiding-recreating-the-initial-state).

**Don’t**
```jsx
const MyComponent = (props) => {
    const [value, setValue] = useState(calculateSomethingExpensive(props));

    return <div>{value}</button>;
}
```

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

    return <div>{value}</button>;
}
```

#### Keep you render [pure](https://react.dev/learn/keeping-components-pure). 
Otherwise it could lead to different bugs such as unpredictable `ref` values.

**Don’t**
```jsx
const MyComponent = (props) => {
    const ref = useRef(props);
    ref.current = props;

    return <div>text</div>;
}
```

**Do**
```jsx
const MyComponent = (props) => {
    const ref = useRef(props);

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

    return <div>text</div>;
}
```

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