はじめに
かつてReactを業務で扱った際の話ですが、全く分からない状態でその業務に携わり、2ヶ月後に別の業務に移ることとなってしまったため、私自身の中でReactがよく分からないままになっていました。
しかし、昨今「Reactは使えて当たり前」という風潮を肌でビシバシ感じるため、簡単なカウンターアプリを作成し理解を整理してみます。
ディレクトリ構成
カウンターアプリのディレクトリ構成は以下です。
src
├─actions
│ └─action.jsx
├─components
│ └─CounterComponent.jsx
├─reducers
│ └─reducers.jsx
├─App.jsx
├─main.jsx
├─store.jsx
index.html
action.jsx
export const INCREMENT = 'INCREMENT';
export const DECREMENT = 'DECREMENT';
export const increment = () => ({
type: INCREMENT
});
export const decrement = () => ({
type: DECREMENT
});
CounterComponent.jsx
import React from 'react';
import { useSelector, useDispatch} from "react-redux";
import { increment, decrement } from "../actions/action";
const CounterComponent = () => {
const count = useSelector(state => state.count);
const dispatch = useDispatch();
return (
<div>
<p>Count: {count}</p>
<button onClick={() => dispatch(increment())}>Increment</button>
<button onClick={() => dispatch(decrement())}>Decrement</button>
</div>
);
}
export default CounterComponent;
reducers.jsx
import { INCREMENT, DECREMENT } from "../actions/action";
const initialState = {
count: 0
};
const counterReducer = (state = initialState, action) => {
switch (action.type) {
case INCREMENT:
return { count: state.count + 1 };
case DECREMENT:
return { count: state.count - 1 };
default:
return state;
}
};
export default counterReducer;
App.jsx
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import CounterComponent from './components/CounterComponent';
const App = () => {
return (
<Provider store={store}>
<CounterComponent />
</Provider>
);
}
export default App;
main.jsx
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)
store.jsx
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './reducers/reducers';
const store = configureStore({
reducer: counterReducer
});
export default store;
index.html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
アプリの流れ
1
main.jsxが読み込まれ、index.htmlのidがrootの要素にAppコンポーネントをレンダリングします。
2
App.jsxで全体をProviderコンポーネントでラップし、CounterComponentコンポーネントを呼び出します。
Providerは、Reactのアプリ全体にReduxの「ストア(アプリ全体の状態を管理する場所)」を提供する仕組みです。Providerがあることで、アプリ中のどのReactコンポーネントでも、そのストアの中にある状態(今回のカウンターアプリではcountの数値)の取得や更新などができるようになります。
<Provider store={store}>
上記はstoreが2回出てきていますが
左側のstoreは、Providerコンポーネントのプロパティ名です。Providerには、storeという名前のプロパティがあります。このプロパティを使って、ReactコンポーネントにReduxのストアを提供します。
右側のstoreは、store.jsで定義したReduxのストアです。このストアは、アプリ全体の状態を管理しています。
つまり、この「store={store}」の構造は、右側のstore(Reduxストア)を左側のstoreプロパティ(Providerのプロパティ)に渡しているという意味になります。
説明が長くなりましたが、要はこれでアプリ中のどのReactコンポーネントでも、そのストアの中にある状態(今回のカウンターアプリではcountの数値)の取得や更新などができるようになるということです。
3
CounterComponent.jsxでは、まずuseSelectorでstateからcountの情報を取得し、画面表示に使用しています。
useSelectorの引数に渡されているstateは、Reduxが管理している「アプリ全体の状態(ストアの状態)」です。Reduxでは、すべての状態がstateというオブジェクトに格納されていて、useSelectorを使うことでその状態を取得することができます。
また、useDispatch()関数(アクションをReduxに通知するための関数)を変数dispatchへ代入し、ボタンがクリックされるたびに、incrementまたはdecrementというアクションをストアに送ります。
Reduxの基本的な流れは以下です。
- アクションをディスパッチ(送信)する。
- ストアにアクションが送られ、リデューサーが実行される。
- リデューサーが状態を更新し、新しい状態がコンポーネントに反映される。
このとき、「どのストアに送られるか」は、Reactコンポーネントがどのストアを使っているかによって決まります。
今回はで使用するストアを指定しているため、dispatch()されたアクションは、このstore.jsで作成したストアに送信されます。
4
action.jsでは、「アクション」という状態を変更するための指示書のようなものを定義しています。
INCREMENTやDECREMENTというアクションタイプ(状態をどう変えるかを示す文字列)と、それを使ってアクションを作る関数(アクションクリエータ)を定義しています。
5
store.jsxのストアではconfigureStore()関数にcounterReducerという関数を渡しています。
このcounterReducerが、状態(今回のカウンターアプリではcountの数値)がどう変わるかを決める重要な部分です。
6
reducers.jsxにて、送られてきたaction.typeがINCREMENTであればcountを1増加、DECREMENTであれば1減少させます。
7
ボタンクリックにより更新されたcountはCounterComponent.jsxで取得することができるため、増減したcountが描画されます。
最後に
まだまだ理解が浅い部分ではあるので、間違っている点などありましたらご指摘いただけますと幸いです!