【React.js】Reduxを使ってstate(状態)を一元管理する。
目次
今回は、Reduxを使ってReactにおいて、stateを一元管理する方法をまとめます。
Reduxを利用することで、コンポーネントの垣根を超えたデータのやり取りが可能になり、stateの一元管理ができるようになります。
Reduxはほとんどのアプリケーションで利用されるような頻出度の高い機能であり、とても便利なものになります。
Reduxとは?
Reduxとは、stateを一元管理できる機能です。
stateを一元管理できるので、コンポーネント間でのデータのやり取りが簡単になります。
コンポーネント間でのデータのやり取りには、propsを用いることができますが、propsだとバケツリレーのようにデータをやり取りしなければならず、コードの見通しがかなり悪くなってしまいます。
Reduxを用いることで、データを一元管理でき、コードの見通しがかなり良くなります。
React hooksのuseStateがコンポーネント内でstate(状態)の管理ができるものですが、Reduxはコンポーネントの垣根を越えて、state(状態)の一元管理が可能になる機能になります。
また、もちろんデータを更新する関数なども一元管理できます。
reactととても相性がいいので、よく利用されるようです。
Reduxの要素
state
stateとは、データやUIの状態などのアプリケーションが保持している情報のことを指します。
useStateの時も説明済みですが、アプリケーションにおいて、データやUIなどのstateは随時更新されます。
store
storeとは、アプリケーション全体のstateを保持するオブジェクトのことを指します。
storeは必ずアプリケーション内に一つしか存在せず、すべてのstateはこの単一のstoreの中に集約されます。
action
actionとは、何が起きたのかの情報を持つオブジェクトのことを指します。
stateに何か更新がある場合は、必ずactionを経由して、storeへ送信されます。そういった流れでstateに更新がかかります。
reducer
reducerとは、stateを更新する関数を保有する場所を指します。
storeから受け取ったstateとactionに応じて、更新されたstateを返します。
dispatch
dispatchとは、reducerに保有されている関数を呼び出すときに使用する機能を指します。
reducerに現在のstateとactionを届けます。
Redux Toolkitについて
Redux Toolkitとは、ReactでReduxをより簡単に利用できるようにするためのライブラリです。
Reduxを単体で使うことは、非常に複雑で難しいです。
このライブラリを使用することでよりシンプルで簡単にReduxを使うことができます。
Redux公式でもこのライブラリを使用することが推奨されています。
今回は、このRedux Toolkitを用いて、Reduxを利用しようと思います。
Reduxの使い方
それでは実際のコードで、Reduxを使ってみようと思います。
今回は、実践的にReduxを用いて、メンバーリストを追加するプログラムを作ってみます。
reactプロジェクトの作成は下記を参考にしてください。
【React.js】React.jsとは?環境構築やHello World表示までをご紹介
ReduxとRedux Toolkitのインストール
まず、ReduxとRedux Toolkitをインストールします。
下記コマンドを実行してください。
//npmの場合
npm install @reduxjs/toolkit react-redux
npm install -D @types/react-redux
//yarnの場合
yarn add @reduxjs/toolkit react-redux
yarn add -D @types/react-redux
今回、typescriptなので、二つコマンドを実行します。
Sliceを用意する
Sliceとは、redux toolkitにおいて、actionとreducerをひとまとめに記述できるものです。
srcディレクトリ直下に「redux」フォルダを作成してください。
そしてその中に「MemberSlice.tsx」ファイルを作成して、下記のように記述してください。
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface Member {
id: string;
name: string;
}
const memberSlice = createSlice({
name: "members",
initialState: [] as Member[],
reducers: { //reducer
addMember: (state, action: PayloadAction<Member>) => {
state.push(action.payload);
},
},
});
export const { addMember } = memberSlice.actions;
export default memberSlice.reducer;
一つずつ解説していきます。
まず下記でstateの型を宣言しています。
interface Member {
id: string;
name: string;
}
下記は、createSliceを使って、Sliceを作成しています。
const memberSlice = createSlice({
name: "members",
initialState: [] as Member[],
reducers: { //reducer
addMember: (state, action: PayloadAction<Member>) => {
state.push(action.payload);
},
},
});
nameでstateの名前を設定、initialStateでstateの初期状態を設定、reducersで各reducerを設定しています。
reducersには、addMemberという、stateにメンバーを追加するreducerを記述しています。
action.payloadとはactionが持つデータのことであり、ここではstateに新たに追加するメンバーのデータのことを指します。
下記は、memberSliceからアクションクリエイターを抽出してエクスポートするものです。
export const { addMember } = memberSlice.actions;
これを記述することで、外部コンポーネントでreducerをインポートすることができるようになります。
storeを用意する
続いて、アプリケーション全体のstateを保持するための箱となるstoreを用意します。
「redux」ディレクトリの中に「store.tsx」ファイルを作成して、下記のように記述してください。
import { configureStore } from "@reduxjs/toolkit";
import MemberReducer from "./MemberSlice";
export const store = configureStore({
reducer: {
members: MemberReducer,
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
ここでは、先ほど作成したmemberSliceをインポートして、Sliceやreducerを組み合わせます。
また、下記部分では、stateやdispatchの型を宣言しています。
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
外部コンポーネントでstateやdispatchを利用する際にこれらの型をインポートします。
Reduxを利用するためのセットアップ
ReactアプリケーションでReduxを利用するためのセットアップを行います。
App.tsxファイルを下記のように書き換えてください。
import { Provider } from "react-redux";
import "./App.css";
import { store } from "./redux/store";
function App() {
return (
<Provider store={store}></Provider>
);
}
export default App;
ここまでで、Reduxの準備は完了です。
次からは、コンポーネント内でStateを表示させたり、Stateに更新をかけたりしていきます。
Stateの値を更新する
Reduxで管理しているStateを更新するプログラムをコンポーネント内で実装してみます。
srcディレクトリ直下に「components」フォルダを作成してください。
その中にForm.tsxファイルを作成し、下記のように記述してください。
import { useState } from "react";
import { useDispatch } from "react-redux";
import { addMember } from "../redux/MemberSlice";
import { AppDispatch } from "../redux/store";
const Form = () => {
const [memberName, setMemberName] = useState("");
const dispatch = useDispatch<AppDispatch>();
const getId = () => {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
let randomStr = "";
for (let i = 0; i < 7; i++) {
randomStr += chars.charAt(Math.floor(Math.random() * chars.length));
}
return randomStr;
};
const handleAddMember = () => {
dispatch(addMember({ id: getId(), name: memberName }));
};
return (
<>
<span>名前</span>:
<input type="text" onChange={(e) => setMemberName(e.target.value)} />
<br />
<br />
<button onClick={() => handleAddMember()}>メンバーを追加する</button>
</>
);
};
export default Form;
inputに入力された名前をボタンクリックでReduxにデータを追加するプログラムです。
inputの入力イベントで、入力された値を一度memberNameのstateに格納します。
そして、ボタンクリックするとaddMemberのreducerを利用してreduxのstateに値を追加します。
この際にnameは入力された値ですが、idはランダムな文字列にしました。
ここでのポイントとしては、「useDispatch」です。
下記のように、reducerを利用するためには、dispatchを使います。
const dispatch = useDispatch<AppDispatch>();
//省略
dispatch(addMember({ id: getId(), name: memberName }));
Stateの値を表示する
Reduxで管理しているStateをコンポーネント内で表示させてみようと思います。
「components」ディレクトリの中にMain.tsxファイルを作成し、下記のように記述してください。
import { useSelector } from "react-redux";
import { RootState } from "../redux/store";
import Form from "./Form";
const Main = () => {
const { members } = useSelector((state: RootState) => state);
return (
<div style={{ padding: "2%" }}>
<Form />
<br />
<br />
<ul>
{members ? (
members.map((member: any, index: number) => (
<li key={index}>
ID:{member.id} 名前:{member.name}
</li>
))
) : (
<tr>
<td colSpan={2}>Loading...</td>
</tr>
)}
</ul>
</div>
);
};
export default Main;
先ほどのForm.tsxをインポートしています。
reduxのstateを取得して、mapを使ってliでループ表示させています。
stateの値を取得するには、「useSelector」を利用します。
const { members } = useSelector((state: RootState) => state);
これで完成です。
最後に、App.tsxでMain.tsxをインポートすることを忘れずに。
import { Provider } from "react-redux";
import "./App.css";
import Main from "./components/Main";
import { store } from "./redux/store";
function App() {
return (
<Provider store={store}>
<Main />
</Provider>
);
}
export default App;
早速ブラウザで動作確認してみてください。
下記のように、inputに入力した名前が一覧で表示されたら、OKです。
Redux Toolkitで非同期処理を扱う方法
Redux Toolkitにおいて、非同期処理を扱う方法はまた違った方法をします。
Reduxで非同期処理を扱う場面としては、例えばWeb APIで取得したデータをReduxのstateに反映させる時などです。
今回はJSONPlaceholderiというサービスを使って、テスト用のユーザー一覧を取得できるREST APIを利用します。
JSONPlaceholder
https://jsonplaceholder.typicode.com/
Sliceの作成とstoreに登録
新たに非同期処理用のSliceを作成します。
「redux」ディレクトリ直下に「UserSlice.tsx」を作成して、その中に下記のように記述してください。
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
export const getUsers = createAsyncThunk("user/getUsers", async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
});
const usersSlice = createSlice({
name: "user",
initialState: {
users: [],
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(getUsers.fulfilled, (state, action) => {
state.users = action.payload;
});
},
});
export default usersSlice.reducer;
下記の部分で、「createAsyncThunk」を利用して、reduxで非同期的にREST APIを実行します。
export const getUsers = createAsyncThunk("user/getUsers", async () => {
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error("Failed to fetch users");
}
const data = await response.json();
return data;
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
});
取得したデータはreturnで返します。
そして、取得したデータをstateのusersに保存したい場合は、下記のように「fulfilled」を使って、「extraReducers」として利用します。「fulfilled」での情報の取得が成功すれば、「action.payload」に入っているデータをusersに設定します。
extraReducers: (builder) => {
builder.addCase(getUsers.fulfilled, (state, action) => {
state.users = action.payload;
});
}
Sliceを作成したら、storeに登録しましょう。
store.tsxに下記のように追記してください。
import { configureStore } from "@reduxjs/toolkit";
import MemberReducer from "./MemberSlice";
import UserReducer from "./UserSlice"; //追記
export const store = configureStore({
reducer: {
members: MemberReducer,
users: UserReducer, //追記
},
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;
データをコンポーネント上に表示する
非同期処理で設定したreduxのstoreを実際にコンポーネント上に表示してみましょう。
「components」ディレクトリ直下に「UserList.tsx」を作成して、下記のように記述してください。
import { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { getUsers } from "../redux/UserSlice";
import { AppDispatch, RootState } from "../redux/store";
const UserList = () => {
const { users } = useSelector((state: RootState) => state.users);
const dispatch = useDispatch<AppDispatch>();
useEffect(() => {
dispatch(getUsers());
}, [dispatch]);
return (
<>
<h2>ユーザー一覧</h2>
{users ? (
users.map((user: any, index: number) => (
<p key={index}>
ID:{user.id} 名前:{user.name} ユーザーネーム:{user.username}
</p>
))
) : (
<div>loading</div>
)}
</>
);
};
export default UserList;
コンポーネント上でのデータの扱い方は同じです。
useEffectを使って、dispatchでgetUsersを実行して、stateにデータを設定しています。
さて、下記のように、App.tsxでUserList.tsxを読み込んで、動作確認をしてみましょう。
import { Provider } from "react-redux";
import "./App.css";
import UserList from "./components/UserList";
import { store } from "./redux/store";
function App() {
return (
<Provider store={store}>
<UserList />
</Provider>
);
}
export default App;
下記のように、取得したユーザーデータの一覧が表示されれば、成功です。
Redux DevTools
Google chormeの拡張機能で「Redux DevTools」というツールがあります。
このツールは、reduxの状態変化のプロセスをディベロッパーツール上で可視化することが可能で、reduxを用いた開発においては非常に役に立つツールになります。
ぜひ導入をご検討ください。
Redux DevTools
https://dev.classmethod.jp/articles/redux-devtools/
上記からインストールが可能です。
インストールが完了したら、ブラウザでreduxを使っているプロジェクトにアクセスしている状態で、「Ctrl + Shift + i」もしくは「F12」でディベロッパーツールを開いてください。(macの場合は、「option + command + i」)
ディベロッパーツールのタブで「Redux」が追加されているので、開いてみてください。
そうすると下記のように、Reduxのプロセスが確認できるようになります。
まとめ
今回は、Reactにて、redux toolkitでReduxを利用する方法について、紹介しました。
Reduxはほぼすべてのアプリケーションで利用されているといっても過言ではないくらい必須の機能で、reactでは必ず押さえておきたい機能になります。
Reduxを用いることで、データの一元管理が可能になり、よりスムーズにデータのやり取りが可能になります。
これにより、より柔軟なアプリケーション開発を可能にしてくれます。
筆者もまだredux初心者なので、マスターしていきたいです。