Search code examples
javascriptreactjszustand

zustand state updated even if only part of the state is changed


I have a React test app that uses Zustand as store. The app only displays an input field but every time the value of the field is updated the whole app is refreshed. I must be doing something stupid in the store but don't seem to find what. Any help would be appreciated.


import React, { useEffect } from 'react'
import { create } from 'zustand'
import { v4 as uuidv4 } from 'uuid';

const store = create((set, get) => ({
  currentPage: { blocks: [] },
  blocksForCurrentPage: [],
  findBlockById: blockId => get().blocksForCurrentPage.find(block => block.blockId === blockId),
  _addDummyBlockAtEndOfPage: _ => {
    const newDummyBlock = ({blockId: uuidv4(), content:'', type:undefined, isDummy: true})
    set(state => ({ blocksForCurrentPage: [...state.blocksForCurrentPage, newDummyBlock] }))
    set(state => ({ currentPage: { ...state.currentPage, blocks: [...state.currentPage.blocks, newDummyBlock.blockId] } }))
  },
  initCurrentPage: _ => {
    const blocks = get().currentPage?.blocks
    const lastBlockId = blocks.length > 0 ? blocks[blocks.length - 1] : undefined
    if (lastBlockId === undefined || !get().findBlockById(lastBlockId)?.isDummy) get()._addDummyBlockAtEndOfPage()
  },
  updateBlock: ({ blockId, type, content }) => {
    //update the block content
    const index = get().blocksForCurrentPage.findIndex(block => block.blockId === blockId)
    if (index < 0) return
    
    set(state => {
      const block = state.blocksForCurrentPage[index]
      const newArray = [...state.blocksForCurrentPage]
      newArray.splice(index, 1, { ...block, type, content, isDummy: false })
      return {blocksForCurrentPage: newArray}
    })
  },
}))

function App () {
  const { currentPage, initCurrentPage, updateBlock } = store((state) => ({
      currentPage: state.currentPage,
      initCurrentPage: state.initCurrentPage,
      updateBlock: state.updateBlock,
    }))
  
  console.log('Redraw app')

  useEffect(() => {
    console.log('in use event')
    initCurrentPage()
  }, [currentPage])
  
  const onChange = (t, blockId) => {
    updateBlock({ blockId, content: t.target?.value  })
  }
  
  return <>{currentPage?.blocks?.map((blockId) => <input key={blockId}
    onChange={t => onChange(t, blockId)}/>)}</>
}

export default App;

The output prints once 'in use event' which is expected to appear only once and that works fine. However the 'Redraw app' is printed every time I press a key with the input box being focused.

Now whenever the value of the input changes I update an item in the blocksForCurrentPage array, but as you see the rendering is only dependant on the currentPage, not the blocksForCurrentPage. Why is the whole App still updating ?

for those willing to help here is the package.json file

{
  "name": "webclient",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "immer": "^9.0.19",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1",
    "uuid": "^9.0.0",
    "zustand": "^4.3.2"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": [
      "react-app",
      "react-app/jest"
    ]
  },
  "browserslist": {
    "production": [
      ">0.2%",
      "not dead",
      "not op_mini all"
    ],
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 safari version"
    ]
  }
}

Solution

  • For future reference, I believe you needed pass in 'shallow'. You're re-rendering an object which doesn't have a stable reference, so its new on every render.

      const { currentPage, initCurrentPage, updateBlock } = store((state) => ({
          currentPage: state.currentPage,
          initCurrentPage: state.initCurrentPage,
          updateBlock: state.updateBlock,
        }, shallow))
    
    or a longer version
    
      const currentPage = store((state) => state.currentPage);
      const initCurrentPage = store((state) => state.initCurrentPage);
      const updateBlock = store((state) => state.updateBlock);