はじめに
Reactでネストの深い構造化されたデータを扱う際に、useStateを使うとデータの更新にスプレッド構文を使用することになりコードが複雑になる場合があります。そのような場合には、useStateよりもuseImmerを使った方がデータの扱いがより簡単になる可能性があると思います。
本記事ではuseImmerを使用して、簡単なカウンターアプリを実装していきます。
環境
- React:18.3.1
- Vite:5.4.1
- immer:10.1.1
- useImmer:0.10.0
npm install immer use-immer
ゴール
use-immerを使いながらを簡単なカウンターを実装してみる
useState と useImmer
useStateとuseImmerを使用して下記のように構造されたデータを扱ってみます。
{
categories: {
A: { name: "カテゴリA", count: 0 },
B: { name: "カテゴリB", count: 0 }
},
totalCount: 0
}
◆ useState
useStateを例にデータを管理する場合、ネストされた構造の更新には少々手間がかかります。
ネストされたデータ(categoriesオブジェクト内のnameやcount)を更新する際、スプレッド構文を使用するとコードが複雑になります。
// 構造化されたデータ例
const [categoryState, setCategoryState] = useState({
categories: {
A: { name: "カテゴリA", count: 0 },
B: { name: "カテゴリB", count: 0 }
},
totalCount: 0
});
// カウントを更新するための関数
const incrementCount = (category) => {
setCategoryState(prev => ({
...prev,
categories: {
...prev.categories,
[category]: {
...prev.categories[category],
count: prev.categories[category].count + 1
}
},
totalCount: prev.totalCount + 1
}));
};
◆ useImmer
次にuseImmerを使用し同様のデータを作成してみます。
useImmerを使用するとコードがより直感的で簡潔になります。
draftオブジェクトを直接変更できるため、複雑なスプレッド構文を使用しなくて済みます。
draftを使って直接的な変更が可能で、更新ロジックがシンプルで読みやすくなります。
const [state, updateState] = useImmer(initialState);
const updateFunction = (draft) => {
// stateを変更
draft.someProperty = newValue;
};
import { useImmer } from 'use-immer';
// 構造化されたデータ例
const [categoryState, updateCategoryState] = useImmer({
categories: {
A: { name: "カテゴリA", count: 0 },
B: { name: "カテゴリB", count: 0 }
},
totalCount: 0
});
// カウントを更新するための関数
const incrementCount = (category) => {
updateCategoryState(draft => {
draft.categories[category].count++;
draft.totalCount++;
});
};
構造化されたデータ、特にネストの深いオブジェクトを扱う場合、useStateよりもuseImmerを使った方がよいと思います。
ソース
useImmerを使ってカテゴリとサブカテゴリの階層構造を持つカウンターサンプルを作ってみます。
次の例はカテゴリ、サブカテゴリ、カウント、合計値のデータ構造を持つオブジェクトをuse-immer管理し、ボタンクリックで各カウンターの値を増やすものです。
import { useImmer } from 'use-immer';
import './App.css';
const App = () => {
// オブジェクトを作成
const [categoryState, updateCategoryState] = useImmer({
categories: {
A: {
name: "カテゴリ A",
subCategories: {
X: { name: "サブカテゴリ X", count: 0 },
Y: { name: "サブカテゴリ Y", count: 0 },
},
total: 0
},
B: {
name: "カテゴリ B",
subCategories: {
Z: { name: "サブカテゴリ Z", count: 0 },
},
total: 0
}
},
AllTotalPoint: 0
});
// カウントを増やす
const increment = (category, subCategory) => {
// 状態を更新
updateCategoryState(draft => {
const categoryData = draft.categories[category];
const subCategoryData = categoryData.subCategories[subCategory];
// サブカテゴリのカウント値+1
subCategoryData.count++;
// カテゴリのトータル値+1
categoryData.total++;
// 全体の合計ポイント値+1
draft.AllTotalPoint++;
});
};
return (
<div className="counter-container">
{/* カテゴリのオブジェクトをループして表示 */}
{Object.entries(categoryState.categories).map(([categoryKey, category]) => (
<div key={categoryKey} className="category">
<h2>{category.name}</h2>
{/* サブカテゴリのオブジェクトをループして表示 */}
{Object.entries(category.subCategories).map(([subKey, subCategory]) => (
<div key={subKey}>
{subCategory.name}: {subCategory.count}
{/* ボタンクリックでインクリメント */}
<button onClick={() => increment(categoryKey, subKey)}>+</button>
</div>
))}
<div>計: {category.total}</div>
</div>
))}
<h3>総ポイント: {categoryState.AllTotalPoint}</h3>
</div>
);
};
export default App;
動作確認
サーバを立ち上げて動作を確認してみます。
npm run dev
次のように +
をクリックすると総ポイント数がインクリメントされている事を確認できました。
参考