この記事を作った動機
単に自分用に、zustand の使い方を今 (2025/9/9) わかっている範囲で、書き出してみるだけ。
環境
- Vite
- React
- TypeScript
- Tailwind CSS
- zustand
概要
まだ全然わかっていないが、私の今 (2025/9/9) イメージとしては、「zustand とは、react で言う状態変数の管理に関するライブラリの一つで、親 -> 子 コンポーネントという流れで状態変数を渡していく以外で、コンポーネント間の状態変数を共有、管理することができるもの。」という感じである。
TODO
- 参照が変わらないと、再描画が起こらないことを書く。例えば、setter 内で、get()したものを直接変更し、set()のところに使うと、参照が同じのため、問題が起こる。
使い方
import
import { create } from "zustand";
宣言例
type DatabaseState = {
websocket: WebSocket | null;
serverIP: string | null;
changeServer: (ip: string) => void;
closeConnection: () => void;
getWebsocket: () => WebSocket | null;
};
export const useDatabaseStore = create<DatabaseState>((set, get) => ({
// 変数の初期化
websocket: null,
serverIP: "ws://localhost:50097",
// 変数を操作する関数群
changeServer: (ip: string) => {
set({ serverIP: ip });
},
closeConnection: () => {
const ws = get().websocket;
ws?.close();
set({ websocket: null });
},
getWebsocket: () => get().websocket,
}));
データを呼び出し、利用する
export function Acompornent(){
// 他の hooks と同様にコンポーネントのコードの先頭あたりに書く必要がある。
const websocket = useDatabaseStore((s) => s.websocket);
console.log(websocket)
// 何かしらのコード
}
関数を呼び出す
export function Acompornent(){
// 他の hooks と同様にコンポーネントのコードの先頭あたりに書く必要がある。
const changeServer = useDatabaseStore((s) => s.changeServer)
changeServer("ws://localhost:88091")
// 何かしらのコード
}
躓いたこと
イミュータブルの原則
zustand を使っているときに、どうやっても値を更新しているにも関わらず、再描画が正しくかからないという問題が起こった。これは、同じ参照を持つ変数を変更するという、“ミュータブル"な扱いをしたときに起こるようである。一度宣言された、作られた変数は原則変更しない(できない)というのが、React や zustand といった状態変数の概念の前提にはあるっぽいと私は最終的に考えた。
今回の場合では、overlayWindow.tsx で動かしているいろんな細かい操作をするためのツールのウィンドウ描画を支援するコード内で起こった。例えば、特定のウィンドウがクリックされると、クリックされた isActive の値が true、それ以外のウィンドウの isActive の値が false になるという処理 makeAwindowActive があるが、値を変更しても正しく連動して動作しないということがあった。症状としては、値は更新されているが、他のウィンドウをクリックしても、useEffect を介して再描画がかからないため、元々アクティブだったウィンドウは inActive スタイルに切り替わらないという症状になった。
今回の例のコードで登場するデータ型
type AoverlayWindow = {
name: string, // window title
UUID: string,
isActive: boolean, // false -> inactive, true -> active
zIndex: number // 200-1200
}
type overlayWindows = {
windows: AoverlayWindow[],
zIndexMax: number,
zIndesMin: number,
addWindow: (window:AoverlayWindow) => void, // in first place, z-index will be the highest number.
removeWindow: (window:AoverlayWindow) => void,
allWindowInactive: () => void, // no need to update z-index
makeAwindowActive: (window:AoverlayWindow) => void,// the active window need to update z-index to the highest number. need to update other windows z-index subtracted by 1.
getWindow: (window:AoverlayWindow) => AoverlayWindow,
}
useEffect の例
const windows = useOverlayWindowStore((s) => s.windows)
// 何かしらのコード ...
useEffect(() => {
console.log("the overlay window info is updated")
if(getWindow(aWindowINIT.current).isActive){
setWindowStyle({
left: String(windowPos.current.x) + "px",
top: String(windowPos.current.y) + "px",
zIndex: String(getWindow(aWindowINIT.current).zIndex),
opacity: String(0.95),
})
}else{
setWindowStyle({
left: String(windowPos.current.x) + "px",
top: String(windowPos.current.y) + "px",
zIndex: String(getWindow(aWindowINIT.current).zIndex),
opacity: String(0.30),
})
}
},[windows])
// 何かしらのコード ...
とりあえず動くレベルのコード
const useOverlayWindowStore = create<overlayWindows>((set,get) => ({
windows: [],
// const vals
zIndesMin: 1300,
zIndexMax: 1400,
// register and deresiger window
addWindow: (window:AoverlayWindow) => {
let newInfo = get().windows
window.isActive = true
window.zIndex = get().zIndexMax
for(let i = 0; i < newInfo.length; i++){
newInfo[i].isActive = false
newInfo[i].zIndex = newInfo[i].zIndex--
}
// これは新しい参照を持つ window という変数があるので、再描画が行われる
// newInfo に関しては、イミュータブルの原則をこのコードでは破ってしまっているが、それでも値の変更自体は反映されるようである。
// ただし newInfo 単体では再描画が正しくかからないことに変わりはない。
set(() => ({windows: [...newInfo,window]}))
},
// 何かしらのコード...
// 注目する部分 -----------------------
// 注目する部分 -----------------------
// window manipulation
allWindowInactive: () => {
let oldInfo = get().windows // 古い参照
let newInfo = [] // 新しく配列を宣言することで、新しい参照を作成する
for(const old of oldInfo){
newInfo.push(old)
}
for(let i = 0; i < newInfo.length; i++){
newInfo[i].isActive = false
}
// 新しい配列 newInfo を設定したので、再描画される
set(() => ({windows: newInfo}))
},
// 注目する部分 -----------------------
// 注目する部分 -----------------------
// 何かしらのコード...
getWindow: (window:AoverlayWindow) => {
console.log("get window")
console.log(window.UUID)
console.log(window)
console.log(get().windows)
for(let test of get().windows){
if(test.UUID == window.UUID) {
return test
}
}
return window
}
}))
問題のあるコード
const useOverlayWindowStore = create<overlayWindows>((set,get) => ({
windows: [],
// const vals
zIndesMin: 1300,
zIndexMax: 1400,
// register and deresiger window
addWindow: (window:AoverlayWindow) => {
let newInfo = get().windows
window.isActive = true
window.zIndex = get().zIndexMax
for(let i = 0; i < newInfo.length; i++){
newInfo[i].isActive = false
newInfo[i].zIndex = newInfo[i].zIndex--
}
set(() => ({windows: [...newInfo,window]}))
},
// 何かしらのコード...
// 注目する部分 -----------------------
// 注目する部分 -----------------------
// window manipulation
allWindowInactive: () => {
let newInfo = get().windows // newInfo は古い参照のまま
for(let i = 0; i < newInfo.length; i++){
newInfo[i].isActive = false
}
// newInfo は元々あった windows 配列を再利用しており、参照が変わっていないため、正しく再描画がここでかからない
set(() => ({windows: newInfo}))
},
// 注目する部分 -----------------------
// 注目する部分 -----------------------
// 何かしらのコード...
getWindow: (window:AoverlayWindow) => {
console.log("get window")
console.log(window.UUID)
console.log(window)
console.log(get().windows)
for(let test of get().windows){
if(test.UUID == window.UUID) {
return test
}
}
return window
}
}))
参考
- Flux inspired practice - Zustand
https://zustand.docs.pmnd.rs/guides/flux-inspired-practice#use-set-/-setstate-to-update-the-store (2025年9月30日) - Immutable state and merging - Zustand
https://zustand.docs.pmnd.rs/guides/immutable-state-and-merging (2025年9月30日) - State change does not cause re-render · pmndrs/zustand · Discussion #1653
https://github.com/pmndrs/zustand/discussions/1653 (2025年9月30日)
ChatGPT
関係ありそうな記事
参考にしたサイトとか
-
Introduction - Zustand
https://zustand.docs.pmnd.rs/getting-started/introduction (2025年9月9日) -
ChatGPT
https://chatgpt.com/ (2025年9月9日) -
Rules of Hooks – React
https://react.dev/warnings/invalid-hook-call-warning (2025年9月10日) -
javascript - Uncaught Error: Rendered fewer hooks than expected. This may be caused by an accidental early return statement in React Hooks - Stack Overflow
https://stackoverflow.com/questions/53472795/uncaught-error-rendered-fewer-hooks-than-expected-this-may-be-caused-by-an-acc (2025年9月10日) -
Flux inspired practice - Zustand
https://zustand.docs.pmnd.rs/guides/flux-inspired-practice#use-set-/-setstate-to-update-the-store (2025年9月30日) -
Immutable state and merging - Zustand
https://zustand.docs.pmnd.rs/guides/immutable-state-and-merging (2025年9月30日) -
State change does not cause re-render · pmndrs/zustand · Discussion #1653
https://github.com/pmndrs/zustand/discussions/1653 (2025年9月30日)