
Привет, друзья!
На днях, бороздя просторы Сети в поисках вдохновения, наткнулся на Zustand, инструмент для управления состоянием React-приложений, наиболее полно (среди более чем многочисленных альтернатив) отвечающий моим представлениям о, если не идеальном, то адекватном современному React state manager (см., например, эту статью).
Рассказу о нем и будет посвящена данная статья. Сначала немного теории, затем немного практики — по традиции, "запилим" простую "тудушку".
Репозиторий.
Если вам это интересно, прошу под кат.
Теория
Установка
yarn add zustand
# or
npm i zustandСоздание хранилища
Хранилище — это хук. В нем можно хранить что угодно: примитивы, объекты, функции. Функция set объединяет (merge) состояние.
import create from 'zustand'
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
decrement: () => set((state) => ({ count: state.count - 1 })),
reset: () => set({ count: 0 })
}))
export default useStoreИспользование хранилища
Хук можно использовать в любом месте приложения (без провайдера!). Компонент будет повторно рендериться (только) при изменении выбранного состояния.
Использование всего хранилища
export default function Counter() {
const { count, increment, decrement, reset } = useStore()
return (
<main>
<h2>{count}</h2>
<div className='btn-box'>
<button onClick={decrement} className='btn decrement'>
-
</button>
<button onClick={increment} className='btn increment'>
+
</button>
<button onClick={reset} className='btn reset'>
0
</button>
</div>
</main>
)
}В данном случае компонент Counter будет повторно рендериться при любом изменении состояния.
Использование частей состояния (state slices в терминологии Redux)
// хук для "регистрации" повторного рендеринга
function useLogAfterFirstRender(componentName) {
const firstRender = useRef(true)
useEffect(() => {
firstRender.current = false
}, [])
if (!firstRender.current) {
console.log(`${componentName} render`)
}
}
function Count() {
const count = useStore(({ count }) => count)
useLogAfterFirstRender('Count')
return <h2>{count}</h2>
}
function DecrementBtn() {
const decrement = useStore(({ decrement }) => decrement)
useLogAfterFirstRender('Decrement')
return (
<button onClick={decrement} className='btn decrement'>
-
</button>
)
}
function IncrementBtn() {
const increment = useStore(({ increment }) => increment)
useLogAfterFirstRender('Increment')
return (
<button onClick={increment} className='btn increment'>
+
</button>
)
}
function ResetBtn() {
const reset = useStore(({ reset }) => reset)
useLogAfterFirstRender('Reset')
return (
<button onClick={reset} className='btn reset'>
0
</button>
)
}
const Counter = () => (
<main>
<Count />
<div className='btn-box'>
<DecrementBtn />
<IncrementBtn />
<ResetBtn />
</div>
</main>
)
export default CounterВ данном случае будет повторно рендериться только компонент Count и только при изменении значения count.
Рецепты
Если мы перепишем приведенный выше пример следующим образом:
function Count() {
const count = useStore(({ count }) => count)
useLogAfterFirstRender('Count')
return <h2>{count}</h2>
}
function Controls() {
const { decrement, increment, reset } = useStore(
({ decrement, increment, reset }) => ({ decrement, increment, reset })
)
useLogAfterFirstRender('Controls')
return (
<div className='btn-box'>
<button onClick={decrement} className='btn decrement'>
-
</button>
<button onClick={increment} className='btn increment'>
+
</button>
<button onClick={reset} className='btn reset'>
0
</button>
</div>
)
}
const Counter = () => (
<main>
<Count />
<Controls />
</main>
)
export default CounterТо компонент Controls будет рендериться при любом изменении состояния (потому что объекты сравниваются по ссылке, а не по значению).
Для решения этой проблемы предназначена функция shallow, поверхностно сравнивающая объекты для определения их идентичности и, как следствие, необходимости в повторном рендеринге компонента.
import shallow from 'zustand/shallow'
function Controls() {
const { decrement, increment, reset } = useStore(
({ decrement, increment, reset }) => ({ decrement, increment, reset }),
/* 