この記事では..
-
npx create-react-app hoge --template typescript
で生成されるReact Typescriptのテンプレートリポジトリを更に簡単にしたリポジトリを作成
- その過程でReactのレンダリングについて説明
- Reduxについても上と同じテンプレートを少しだけ修正したシンプルなリポジトリを作成
- その過程でReduxを使わない場合の実装と比較を行い、Reduxが疎結合を実現する様子を確認
参考
・https://qiita.com/FarStep131/items/ad834facc57a443a9dc3
こちらが大変分かりやすい。
React+Typescript 環境構築
Homebrewを使ってNode.jsをインストール。手順はこちらの通り
brew install node@20
完了すると、echo 'export path="/opt/homebrew/opt/node@20/bin:$PATH"' >> ~/.zshrc
の様な形でパスを通すよう表示がされるので、指示通りにパスを通す。
TypeScriptも使える様にしておく。
npm install -g typescript
その後、以下のコマンドでReactテンプレートリポジトリを落としてくる。
npx create-react-app hoge --template typescript
テンプレートのファイル構成
Typescript+Reactテンプレートプロジェクトの構成は以下の様になっている。
hoge/
├── src/
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ └── setupTests.ts
├── public
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── node_modules
│ └── 略
└── tsconfig.json
└── package-lock.json
└── package.json
└── README.md
この初期状態のままhoge/
直下でnpm start
コマンドを実行すると、以下の様な画面になる。
テンプレートの中身
エントリポイントは?
npm start
を実行した時、最初にsrc/index.tsx
が呼び出される。テンプレートの中のpackage.json
ファイルにて、以下のように、npm start
が実行されるとreact-scripts start
が実行されている。このreact-scripts
で内部的にwebpack
やBabel
といったライブラリを使用しており、そこでsrc/index.tsx
をentryとして設定している。
...
"scripts": {
"start": "react-scripts start"
...
}
ちなみに、直接"react-scripts start"をターミナル上で以下の様に実行することも可能である。
./node_modules/.bin/react-scripts start
エントリポイント(src/index.tsx
)の中身は?
エントリポイントであるsrc/index.tsx
ファイルは以下の様に記述されている。
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
ここでは、
public/index.html
に記述されているDOM要素を識別するroot
というIDに、ReactコンポーネントApp
を追加する"レンダリング"
が行われている(具体的な説明は後述)。
もう少し細かい説明
HTMLやXMLの情報はDOM(Document Object Model)によってツリー構造で表現されている。例えば
<!DOCTYPE html>
<html>
<head>
<title>Example Page</title>
</head>
<body>
<div>
<h1>Hello, World!</h1>
<p>This is a paragraph.</p>
</div>
</body>
</html>
のようなHTMLファイルは以下の画像のようにブラウザ上で表示される。
DOMというツリーにおけるノードとは、<html>や、<head>、<h1>の様なHTMLを構成する要素に対応している。つまりDOMではhtml
を1つの頂点とするツリーである。基本的にHTMLではhead
ノードとbody
ノード1つずつから構成される。
head
ではページのタイトルを指定してブラウザのタブに表示させるテキスト(今回だと"Example Page")を設定できたり、HTMLページの見た目を整えるCSSやタブに表示させるアイコン(ファビコン)を指定したりできる。body
ではブラウザの画面上に表示させるコンテンツを指定する。今回だと、サイズの違う"Hello, World!"と"This is a paragraph."が表示されている。
ここでレンダリングとは、このHTML上の特定の要素にReactコンポーネントを追加する作業を意味している。そもそもなぜReactコンポーネントを追加するかというと、
- ブラウザに対するユーザの入力などによる、状態やプロパティの変化に応じて動的にUIを更新できる
- よく使うUIパーツを簡単に使い回せる
- 直接DOM操作をするよりも簡単にUIの変更を管理できる
などの利点があるからである。これを踏まえ、テンプレートのエントリポイントのコードに立ち返る。src/index.tsx
の中で、
...
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
...
なる操作が行われていた。ここでdocument
は特にファイル内で明示的に定義がされていなくても使えるグローバルオブジェクトで、現在表示されているHTMLドキュメント全体(ここではテンプレートのpublic/index.html
)を表している。npm start
で実行されているreact-scripts
は内部的にWebpack Dev Server
を起動しており、このサーバがhttp://localhost:3000
にアクセスしたときにpublic/index.html
を提供するよう設定してある。
この操作は、public/index.html
というdocumentの中で"root"というIDが付いたDOM要素を取得しているのである。ちなみにテンプレートのpublic/index.html
の中では、
<!DOCTYPE html>
<html lang="en">
<head>
...略...
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
...略...
</body>
</html>
の様になっており、"root"というIDが付いたpublic/index.htmlのDOM要素<div>
に、レンダリングによってReactコンポーネントであるAppが以下の様に追加される。
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
ここでReact.StrictModeは開発過程でのみ使用される、潜在的な問題を見つけやすくするためのラッパーであるため今は無視する。ここでは別ファイルで定義し、import App from './App';
でインポートしているAppというコンポーネントを追加しているが、ここで何をしているかは一旦考えず、テンプレートよりも更に簡単な静的サイトを作ってみる。
テンプレートより更に簡単な例を作る
ここでは、さっき"Hello, World!"と"This is a paragraph"を表示したHTML(hoge.html
)と同じものを、Reactコンポーネントを追加するレンダリングを通じて実装する。そのためにはテンプレートの中でsrc/App.tsxの中身を以下に置き換えるだけでいい。
const App = () => {
return (
<div>
<h1>Hello, World!</h1>
<p>This is a paragraph.</p>
</div>
);
};
export default App;
ちなみにDOM要素のIDの名前は好きに決められるので、public/index.html
と同期さえ取れていれば、別に"root"じゃなくても良い。結果としてhoge.html
と同じ、以下のような出力画面になる(全然Reactを使う意味のない静的サイトが生成される)
Reduxも使ってみたい
環境構築
テンプレートにはredux, react-reduxは入っていないので、これらをインストールする。(この作業はテンプレートを生成する度に以下の様に手動で行わなければならない点に注意)
npm install redux react-redux
概要
Reduxはアプリケーションの状態を一元管理してくれるツールである。あるデータ(これを状態と呼ぶ)をStoreに登録することで、アプリを構成するどのコンポーネントからでも同じ様に登録データにアクセスすることが可能となる。
ストアに登録する実体はReducerにある。Reducerは、状態とアクションの二つを引数にとって、「あるアクションが来たら状態をどう変更するか」を規定する。あるアクションが起きた時にそれをReduxに認識させることをdispatchと呼ぶ。流れとしては以下の様な形になる。
Action -> Dispatch -> ReducerがStateを更新 -> UI上で反映
具体的な実装を見ないと分からないので、例を見ていく。
簡単な例 (Reduxを使わない場合..)
以下では、次の画像のような"カウントを増やす"ボタンを押すと数字が増える簡単なカウンタを実装する。最初は敢えてReduxは使わずに実装してみる。
この実装は、テンプレート初期状態からsrc/App.tsx
を以下に置き換えるだけで良い。
import React, { useState } from 'react';
const CounterDisplay: React.FC<{ count: number }> = ({ count }) => {
return <h1>現在のカウント: {count}</h1>;
};
const CounterControl: React.FC<{ increment: () => void }> = ({ increment }) => { //const CounterControl = ({ increment }) => {
return <button onClick={increment}>カウントを増やす</button>;
};
const App: React.FC = () => {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
return (
<div>
<CounterDisplay count={count} />
<CounterControl increment={increment} />
</div>
);
};
export default App;
ここでconst [hoge, fuge] = useState(XX)
は、Reactの状態管理のためのフックである。状態hoge
に関数fuge
を作用させることで、状態の更新をReactに認識させ、UI上で反映させる再レンダリングが行われる。ここでは、状態として変数count
が定義されており、それを関数increment
が更新する。src/App.tsx
内での役割は以下の画像のように理解できる。
あくまでApp内でCounterDisplay
とCounterControl
の紐付けは行われており、その様子は以下の様にまとめられる。
このように、これほど簡単なアプリでも、コンポーネント間で情報の受け渡しが発生していることがわかる。CounterDisplay
はcount
を、CounterControl
はincrement
をAppから引数として渡されている。この例なら全然問題ない気もするが、もっと規模の大きいアプリをReactで作る際にはコンポーネント間での情報の受け渡しは密結合となり、管理が煩雑になってしまう。
簡単な例 (Reduxを使った場合)
上と同じことをReduxを使って実装する。そのためにまずテンプレートリポジトリのsrc/App.tsx
を編集し、さらにsrc/redux/
というディレクトリを作成して3つの新しいファイルを追加する必要がある。作成後のテンプレートリポジトリの構成は以下の様になる。
hoge/
├── src/
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx <-- Modified!!
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ ├── setupTests.ts
│ └── redux/ <-- New!!
│ ├── action.ts
│ ├── reducer.ts
│ └── store.ts
├── public/
│ ├── favicon.ico
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── node_modules/
│ └── 略
└── tsconfig.json
└── package-lock.json
└── package.json
└── README.md
まず、src/App.tsx
を以下の様に書き換える。
import React from 'react';
import { Provider } from 'react-redux';
import store from './redux/store';
import { useDispatch } from 'react-redux';
import { useSelector } from 'react-redux';
import { increment } from './redux/action';
const CounterDisplay: React.FC = () => {
const count = useSelector((state: { count: number }) => state.count);
return <h1>現在のカウント: {count}</h1>;
};
const CounterControl: React.FC = () => {
const dispatch = useDispatch();
const handleIncrement = () => {
dispatch(increment());
};
return <button onClick={handleIncrement}>カウントを増やす</button>;
};
const App: React.FC = () => {
return (
<Provider store={store}>
<div>
<CounterDisplay />
<CounterControl />
</div>
</Provider>
);
};
export default App;
さらに、reduxに関して必要な設定を以下のように3つのファイルで行う。
const initialState = {
count: 0,
};
const counterReducer = (state = initialState, action: { type: string }) => {
switch (action.type) {
case 'INCREMENT':
return {
...state, count: state.count + 1 };
default:
return state;
}
};
export default counterReducer;
export const increment = () => {
return {
type: 'INCREMENT', // アクションタイプ
};
};
import { createStore } from 'redux';
import counterReducer from './reducer';
const store = createStore(counterReducer);
export default store;
今回、Reduxは何を管理してくれていのるか?
まず、今回src/App.tsx
内でProviderを使ってCounterDisplay
とCounterControl
の2つを囲んでいる。ProviderはReduxのメソッドで、引数としてStoreを設定することで、Providerで囲ったコンポーネントにStoreを共有することができる。
Storeは今回何をどう定義しているか?まずReducerは、src/redux/reducer.ts
より、Stateとして"カウント"を管理している。初期値を0とし、actionで定義されている"INCREMENT"がDispatchされた時に、+1を行い、+1されたカウントがReducerの返り値となることがわかる。App側ではCounterControl
メソッドはボタンが押された時にReduxストアのアクションをDispatch
により更新し、CounterDisplay
メソッドはReduxストアのState(現在のReducerの返り値)をuseSelector
により取得している。
この様なStore登録、参照システムであるReduxを使うことで、Reduxを使わない場合には存在していた、App
- CounterControl
間とApp
- CounterDisplay
間の変数の受け渡しは無くなっていることがわかる(Reduxを使った場合ではCounterControl
もCounterdisplay
も引数が無くなっている:下図参照)。この様に、Reduxを使うことで、新しくReduxストアに登録する手間は増えるものの、アプリを構成するコンポーネント間での情報を同期させ、それらの間の結合を疎にすることができる。
まとめ
-
レンダリングは、HTMLやXMLを構成するツリー状のDOM要素にREACTコンポーネントを追加することで、動的にReactコンテンツをUIに反映させるための仕組みである
-
Reduxは、共有したいデータ(State)とActionをStoreに登録しておき、ReducerがDispatchされたActionとStateをもとにStateの更新を行うことでコンポーネント間でデータの授受を行わなくても同期を取り、コンポーネント間の疎結合を実現する仕組みである