React Experiments

Referential Equality


const [items, setItems] = useState(["item", "item"])

return (
  <>
    <div>
      {items.map((item, i) =>
        <div key={i}>{item}</div>
      )}
    </div>
    <button onClick={() => {
      const newItems = items
      newItems.push("item")
      setItems(newItems)
      console.log(newItems)
    }}>Add Item</button>
  </>
)
  

No re-render (object)

Creates a shallow copy pointing to the same memory location. No re-render since React considers it unchanged (same address).


const [items, setItems] = useState(["item", "item"])

return (
  <>
    <div>
      {items.map((item, i) =>
        <div key={i}>{item}</div>
      )}
    </div>
    <button onClick={() => {
      const newItems = [...items]
      newItems.push("item")
      setItems(newItems)
    }}>Add Item</button>
  </>
)
  

Successful re-render

Using the spread operator creates a deep copy of the object. Points to a new memory location.

Reducer State Management

Item

State actions stored in a reducer

Reducers combine state change logic (different event handlers, etc.) into a single function, called by dispatches.

Kanban Board Re-Renders


const Board = () => {
  const [items, setItems] = useState([[1, 2, 3], [4, 5], [6]])

  const moveLeft = ({ item, col }: {...}) => {...}
  const moveRight = ({ item, col }: {...}) => {...}

  return (
    <DndContext
      sensors={sensors}
      collisionDetection={closestCenter}
      onDragStart={handleDragStart}
      onDragEnd={handleDragEnd}>
      <SortableContext items={items[0]} >
        {items[0].map((item) => (
          <Item id={item} key={item} />
        ))}
      </SortableContext>
      <SortableContext items={items[1]} >
        {items[1].map((item) => (
          <Item id={item} key={item} />
        ))}
      </SortableContext>
      <SortableContext items={items[2]} >
        {items[2].map((item) => (
          <Item id={item} key={item} />
        ))}
      </SortableContext>
    </DndContext>
  )
}

const Item = ({id, ...} : {id: number, ...}) => {
  return (
    <div
      ref={setNodeRef}
      style={style}
      {...attributes}
      {...listeners}>
      <Text>
        {id}
      </Text>
      <Button
        onClick={...}
        icon={... ? <ArrowRight /> : <ArrowLeft />}
      />
      <Button
        onClick={...}
        icon={... ? <ArrowRight /> : <ArrowLeft />}
      />
    </div>
  )
}
  

Unoptimized, re-renders entire board

The entire board re-renders for all updates. Unaffected items/columns always re-render, but it's often unnecessary.

1

2

3

4

5

6

Memoizing components

Each column is memoized, stopping unnecessary re-renders. It takes an object prop, but memo() uses shallow comparison so it's not 'equal'. Using the useMemo hook caches the object correctly.

1

2

3

4

5

6

Final optimization with useCallback

Like how defining objects returns a new object each time, defining functions does the same. Callbacks are memoized functions.