zustandを触る
JavaScript, ReactFlux ライクなステート管理ライブラリの zustand
を触った。
https://github.com/react-spring/zustand
サンプルはこちら
https://github.com/grgr-dkrk/zustand-sample
インストール
shell
npm install zustand
基本
create, set
create
を用いて、useStore Hook を作成する。
createの第一引数には set
が入り、こちらで state の更新をする。
src/store.ts
import create from 'zustand';
type AppState = {
count: number;
increase: () => void;
reset: () => void;
};
export const [useStore] = create<AppState>(set => ({
count: 0,
increase: () => set(state => ({ count: state.count + 1 })),
reset: () => set({ count: 0 }),
}));
async/await も使える。
src/store.ts
export const [useStore] = create<AppState>(set => ({
count: 0,
increase: () => set(state => ({ count: state.count + 1 })),
// async / await
reset: async () => {
const resetCount = await Promise.resolve(0);
set({ count: resetCount });
},
}));
コンポーネントで、useStore
を読み込む。
src/components/DisplayCount.tsx
import * as React from 'react';
import { useStore } from '~/store';
const DisplayCount: React.FC = () => {
const count = useStore(state => state.count);
return <p aria-live="polite">{count}</p>;
};
export default DisplayCount;
src/components/CountControll.tsx
import * as React from 'react';
import { useStore } from '~/store';
const CountControll: React.FC = () => {
const controll = useStore();
return (
<React.Fragment>
<button onClick={controll.increase}>カウントを1増やす</button>
<button onClick={controll.reset}>リセット</button>
</React.Fragment>
);
};
export default CountControll;
src/app.tsx
import * as React from 'react';
import DisplayCount from './components/DisplayCount';
import CountControll from './components/CountControl';
export const App = () => {
return (
<React.Fragment>
<DisplayCount />
<CountControll />
</React.Fragment>
);
};
Shallow Equal は、shallow
を第三引数に指定する。
src/components/DisplayPersonData.tsx
import * as React from 'react';
import shallow from 'zustand/shallow';
import { usePersonStore } from '~/store';
const DisplayPersonData: React.FC = () => {
const { name, age } = usePersonStore(
state => ({ name: state.name, age: state.name }),
shallow
);
return <p>{name}{age}</p>;
};
複数のStore
単一ではなく、複数の Store を作れる。
src/store.ts
import create from 'zustand';
type UseCredentialsState = {
currentUser: string;
};
type UsePersonState = {
persons: {
[key: string]: string;
};
};
export const [useCredentialsStore] = create<UseCredentialsState>(set => ({
currentUser: 'jiro',
}));
export const [usePersonStore] = create<UsePersonState>(set => ({
persons: {
taro: '太郎',
jiro: '二郎',
saburo: '三朗',
},
}));
src/DisplayUser.tsx
import * as React from 'react';
import { useCredentialsStore, usePersonStore } from '~/store';
const DisplayUser: React.FC = () => {
const currentUser = useCredentialsStore(state => state.currentUser);
const person = usePersonStore(state => state.persons[currentUser]);
// usePersonStoreのpersons['jiro']を表示
return <p aria-live="polite">{person}</p>;
};
export default DisplayUser;
Redux
zustand は Middleware を備えている。
標準では redux
Middleware が搭載されていて、Redux 風に書ける。
まず、FSA な reducer(state と action を含む)を書く。
src/reducers/Counter.ts
/**
* State
*/
type CounterState = {
count: number;
};
export const initialState: CounterState = {
count: 0,
};
/**
* Action Types
*/
const ADD_COUNT = 'ADD_COUNT' as const;
const RESET_COUNT = 'RESET_COUNT' as const;
/**
* Actions
*/
export const addCount = (payload: CounterState['count']) => ({
type: ADD_COUNT,
payload,
});
export const resetCount = () => ({
type: RESET_COUNT,
});
type ActionsType = ReturnType<typeof addCount> | ReturnType<typeof resetCount>;
/**
* Reducer
*/
export const CounterReducer = (
state = initialState,
action: ActionsType
): CounterState => {
switch (action.type) {
case ADD_COUNT:
return {
...state,
count: state.count + action.payload,
};
case RESET_COUNT:
return {
...state,
count: 0,
};
default:
const _: never = action;
return state;
}
};
store では、create 関数の仮引数に redux
Middleware を指定する。
src/store.ts
import create from 'zustand';
import { redux } from 'zustand/middleware';
import {
CounterReducer,
initialState,
CounterState,
ActionsType as CounterActionsType,
} from './reducers/Counter';
export const [useCounterStore] = create<
CounterState & { dispatch: (action: CounterActionsType) => void }
>(redux(CounterReducer, initialState));
先ほどまでは、create のジェネリクスに State の型を指定していた。
しかし redux
Middleware を挟んだ create の戻り値には、State だけでなく dispatch も含まれる。
そのため上の例では dispatch の型を気持ちだけ追加している。
Component では、作成した useCounterStore の dispatch を使う。
src/components/CounterControll.tsx
import * as React from 'react';
import { useCounterStore } from '~/store';
import { addCount, resetCount } from '~/reducers/Counter';
const CounterControll: React.FC = () => {
const { dispatch } = useCounterStore();
return (
<React.Fragment>
<button onClick={() => dispatch(addCount(1))}>カウントを1増やす</button>
<button onClick={() => dispatch(resetCount())}>リセット</button>
</React.Fragment>
);
};
export default CounterControll;
devtools
Middlware を噛ませることで、Redux DevTools が使用できる。
src/store.ts
import { redux, devtools } from 'zustand/middleware';
import {
CounterReducer,
initialState,
CounterState,
ActionsType as CounterActionsType,
} from './reducers/Counter';
export const [useCounterStore] = create<
CounterState & { dispatch: (action: CounterActionsType) => void }
>(devtools(redux(CounterReducer, initialState)));
これで、Redux のように書けることがわかった。
subscribe
subscribe を自前で用意し、React の外側で使用できる。 しかし内部的には React の Hooks でやっていて、React 自体は必要だった。
とりあえず HTML + TypeScript + Parcel でやった。
index.html
<!DOCTYPE html>
<html lang="ja">
<body id="body">
<p id="displayCount"></p>
<button id="addButton">カウントを1増やす</button>
<button id="resetButton">リセットする</button>
</body>
</html>
index.ts
import create from 'zustand';
const displayCount = document.getElementById('displayCount')!;
const addButton = document.getElementById('addButton')!;
const resetButton = document.getElementById('resetButton')!;
type AppState = {
count: number;
};
export const [_, api] = create<AppState>(set => ({
count: 0,
}));
displayCount.textContent = '0';
const unSubscribeCount = api.subscribe<AppState['count']>(
count => {
displayCount.textContent = count + '';
},
state => state.count
);
addButton.addEventListener('click', () => {
api.setState({ count: api.getState().count + 1 });
});
resetButton.addEventListener('click', () => {
api.setState({ count: 0 });
});