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"
]
}
}
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);