Key points about state management and rendering for better optimisation in React

What I present is something I have experienced, read or learnt, therefore may not be the most comprehensive optimisation guide but I am sure you will learn something. Focus is mainly on functional components and React hooks. And now without further ado

React.memo & React.useMemo & React.useCallback

When rendering Components with child components, make sure to wrap the child components in with React.memo to prevent unnecessary re-renders(the child won't be updated when there is no change affecting it). However this will not apply if the prop passed is not a primitive(number, string, boolean), objects and functions won't be handled because after every rerender a new reference is created therefore react will think it is different and hence rerender the child. Functions also have a similar behaviour To fix this we have to memoize the object prop using React.useMemo and useCallback hook for functions.
A simple example for useMemo

const Parent = () => {
  const [count, setCount] = useState(0)
  const [message, setMessage] = useState('')
  const userInfo = {
    name: "Rogha",
    gender: "male"
  }
  const sayHello = () => setMessage("Katumba Oyeee")
  console.log("Log from parent")
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Add #{`${count} + 1`}</button>
      <span>{message}</span>
      <Child />
    </div>
  )
}

const Child = () => {
  console.log("Log from child")
  return <div>Hello from child<div/>
}

For now, don't mind userInfo and sayHello we'll use them later. If we run the app and click the Add #1 button we notice from the logs that both the child and parent component ran. The child is rerendered even if it is not being updated. Wrapping it with memo fixes the issue

const Child = React.memo(() => {
  console.log("Log from child") 
  return <div>Hello from child<div/>
})

Example of useMemo: say we pass an object userInfo to the child(even if we don't use it)

// In Parent component
 return (
    <div>
      <button onClick={() => setCount(count + 1)}>Add #{`${count} + 1`}</button>
      <span>{message}</span>
      <Child user={userInfo} />
    </div>
  )
}

const Child = React.memo(() => {
  console.log("Log from child")
  return <div>Hello from child<div/>
})

When the parent re-renders the object reference will change, so when react tries to compare the two reference(current and previous), it will think that the props have changed and therefore rerendering the child. We need to wrap the object with React.useMemo to fix this reference issue. It takes two arguments: a callback function returning the object you want to memoize and a dependency list

  const userInfo = React.useMemo(() => ({
    name: "Rogha",
    gender: "male"
  }), [])

Same for the function if we pass it to the child, a re-render will be caused because another reference being created once the parent rerenders. to fix this in functions we wrap our function in the React.useCallback hook. It takes two arguments: the function we are memoizing and a dependency list

const sayHello = React.useCallback(() => setMessage("Katumba Oyeee"), [])

useState and useReducer

useState/useReducer will always cause a rerender of a functional component except for one scenario and that is after the first render when we try to update a state value with the same value e.g if our initial value is 0 and we attempt to set state to 0 there won't be a re-render. Note that this will not hold after prior rerenders, in other words, if we set the value to 1 and then again to 1 for the second time, there will be a rerender but only for the parent component. Child components will not be rerendered as this rerender is just used as a safety precaution

These are both great for managing state in functional components. useState hook is pretty much the first thing every dev learns when learning hooks, If you have used redux you should already be familiar with reducers. useReducer hook takes a reducer and the initial state as arguments and returns the current state and a dispatch function.

const [state, dispatch] = useReducer(reducer, initialState)

useState and useReducer can both be used to accomplish the same outcome, but the ideal usage would be to use the useState hook for simple data like primitives and useReducer for objects or any complex data like a list of objects. How your redux state object looks should give an overview of the type of data that can be used with useReducer.

Render and rerender - what happens

On the initial render, the code you write is converted into React elements using createElement function which returns javascript objects which are then painted on the DOM. The painting phase is called committing, while the first is called rendering. During the rerendering, react looks through the component tree while flagging the children which need changes. The flagged children and all those affected by these children are collected and converted into javascript objects, the last rendered changes and current rerendered changes are compared in a process called reconciliation, a list of all changes is then submitted to update the DOM.

Rerendering doesn't necessarily mean updating the UI/DOM. It simply means a change in the state either by useState or useReducer

State Immutability

Immutability in react is important because of how objects and arrays behave. Unlike primitives objects and arrays are stored as references, this is why if you try to check if different objects with the same data are equal, your answer will always be false because you are not checking if that data is equal but rather if it is pointing to the same reference.

const initialUser = {
    name: "Rogha",
    role: "admin"
}

const [user, setUser] = useState{initialUser)

const handleUserChange = () => {
    user.name = "Martin"
    user.role = "super admin"
    setUser(user)
}

If we call handleUserChange() the user will not update because it's still pointing to the same reference (user), it doesn't matter what data we add or change no update will be made. When react tries to compare the new and old data all it sees is the same reference so it assumes no change has been made. However, if we create a new object with a different name react will know that a new change has been made

const handleUserChange = () => {
    newUser = {...user} //copy user data
    newUser.name = "Martin"
    newUser.role = "super admin"
    setUser(newUser)
}

Handle unnecessary re-renders (without using memo)

To avoid unnecessary re-renders from child components pass the children as props to the parent components. Instead of having a self-closing component we open it and pass the child component and handle it in the children prop.

import Parent from '../Parent'
import Child from '../Child'
return (
  <Parent>
    <Child />
  </Parent>
)
// Inside the Parent component
const Parent = () => (
  <div>
    {children}
  </div>
)

This option should be prioritized to memo because memo is an expensive process(since it performs a shallow comparison before deciding whether to render the component), hence the reason why React does not wrap memo over every child component by default because it is detrimental to the performance of the app in a whole if overly used. The best use case for memo is in components whose props rarely change.
Scenario to understand how memo works

  • Imagine a child component wrapped in memo
  • Your component takes 7ms to render
  • memo takes 2ms to perform a shallow comparison
  • This will mean if there is a change in props the component will take 9ms in total
  • If there is no change in props it will take 2ms(memo time)
  • Now imagine if we frequently change the props - it will take to long to rerender the content.
    Therefore for components with multiple prop changes should use the above option, whereas those with lesser prop changes should use memo

There is no point of using memo if you are passing it children, it will always rerender

For Example

<MemoizedChild>
    <div>Hey</div>
</MemoizedChild>

Dan Abramov has a great example in his article here and another good solution for optimization.

useContext

The ways in which a functional component renders include useState, useReducer, rendering of parent component and useContext. The useContext hook helps prevent prop drilling which involves a parent component passing props through subsequent children which don't use them to those that do. The tradeoff here is that all child components will re-render even if they don't use the passed props. A solution would be to wrap the immediate child of the context provider with memo to prevent unnecessary renders, only the children consuming the context value will be rendered.

const DataContext = React.createContext()

const Parent = () => {
 const userInfo = {
    name: "Rogha",
    role: "admin"
  }
  return (
    <DataContext.Provider value={userInfo}>
      <h1>Provider</h1>
      <ChildOne />
    </DataContext.Provider>
  )
}

const ChildOne = React.memo(() => (
  <>
    <h1>ChildOne</h1>
    <ChildTwo />
  </>
))

const ChildTwo = () => (
  <>
    <h1>ChildTwo</h1>
    <ReceivingChild />
  </>

const ReceivingChild = () => {
  const {name, role} = React.useContext(DataContext)
  return (
    <>
      <h1>ReceivingChild</h1>
      <h2>{`${name} is the ${role}`}</h2>
    </>
  ) }

If the parent re-renders only <ReceivingChild /> will rerender. Another alternative to useMemo with context would be how we handled unnecessary renders without memo from above)

const Parent = () => {
 const userInfo = {
    name: "Rogha",
    role: "admin"
  }
 return (
    <DataContext.Provider value={userInfo}>
      <h1>Provider</h1>
      {children}
    </DataContext.Provider>
  )
}

const ChildOne = () => (
  <Parent>
    <h1>ChildOne</h1>
    <ChildTwo />
  </Parent>
)

Don't use the index as Key when rendering a list

Why this matters is in how React renders lists, if we don't assign keys then when updating, the entire list will rerender as opposed to adding the new items according to their keys. Before updating react checks if the key already exists and only adds those not in the list. Keys give items a stable identity which makes it's easy for React to add/remove/update items in a list.

To get rid of the error each child in an array should have a unique key we are always tempted to use the index as key, which works but creates bugs in some cases. These cases include adding an item at the beginning or inside of a list, reordering the list(this can be sorting, filtering or anything that changes the order of the list).

Here is an example, we