2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

React・Reduxを使った一番シンプルなリポジトリ【入門】

Posted at

この記事では..

  • 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コマンドを実行すると、以下の様な画面になる。
スクリーンショット 2024-10-13 16.32.35.png

テンプレートの中身

エントリポイントは?

npm startを実行した時、最初にsrc/index.tsxが呼び出される。テンプレートの中のpackage.jsonファイルにて、以下のように、npm startが実行されるとreact-scripts startが実行されている。このreact-scriptsで内部的にwebpackBabelといったライブラリを使用しており、そこでsrc/index.tsxをentryとして設定している。

package.json
...
"scripts": {
  "start": "react-scripts start"
  ...
}

ちなみに、直接"react-scripts start"をターミナル上で以下の様に実行することも可能である。

./node_modules/.bin/react-scripts start

エントリポイント(src/index.tsx)の中身は?

エントリポイントである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)によってツリー構造で表現されている。例えば

hoge.html
<!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ファイルは以下の画像のようにブラウザ上で表示される。

スクリーンショット 2024-10-13 21.51.14.png

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の中で、

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の中では、

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が以下の様に追加される。

src/index.tsx
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の中身を以下に置き換えるだけでいい。

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を使う意味のない静的サイトが生成される)

スクリーンショット 2024-10-13 23.10.54.png

Reduxも使ってみたい

環境構築

テンプレートにはredux, react-reduxは入っていないので、これらをインストールする。(この作業はテンプレートを生成する度に以下の様に手動で行わなければならない点に注意)

npm install redux react-redux

概要

 Reduxはアプリケーションの状態を一元管理してくれるツールである。あるデータ(これを状態と呼ぶ)をStoreに登録することで、アプリを構成するどのコンポーネントからでも同じ様に登録データにアクセスすることが可能となる。
 ストアに登録する実体はReducerにある。Reducerは、状態とアクションの二つを引数にとって、「あるアクションが来たら状態をどう変更するか」を規定する。あるアクションが起きた時にそれをReduxに認識させることをdispatchと呼ぶ。流れとしては以下の様な形になる。

Action -> Dispatch -> ReducerがStateを更新 -> UI上で反映

具体的な実装を見ないと分からないので、例を見ていく。

簡単な例 (Reduxを使わない場合..)

 以下では、次の画像のような"カウントを増やす"ボタンを押すと数字が増える簡単なカウンタを実装する。最初は敢えてReduxは使わずに実装してみる。

スクリーンショット 2024-10-14 14.57.29.png

この実装は、テンプレート初期状態からsrc/App.tsxを以下に置き換えるだけで良い。

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内での役割は以下の画像のように理解できる。
スクリーンショット 2024-10-14 15.42.32.png

あくまでApp内でCounterDisplayCounterControlの紐付けは行われており、その様子は以下の様にまとめられる。

スクリーンショット 2024-10-14 15.45.34.png

このように、これほど簡単なアプリでも、コンポーネント間で情報の受け渡しが発生していることがわかる。CounterDisplaycountを、CounterControlincrementを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を以下の様に書き換える。

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つのファイルで行う。

src/redux/reducer.ts
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;
src/redux/action.ts
export const increment = () => {
  return {
    type: 'INCREMENT',  // アクションタイプ
  };
};
src/redux/store.ts
import { createStore } from 'redux';
import counterReducer from './reducer';

const store = createStore(counterReducer);

export default store;

以上により、同じ出力が得られることになる。
スクリーンショット 2024-10-14 14.57.29.png

今回、Reduxは何を管理してくれていのるか?

 まず、今回src/App.tsx内でProviderを使ってCounterDisplayCounterControlの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を使った場合ではCounterControlCounterdisplayも引数が無くなっている:下図参照)。この様に、Reduxを使うことで、新しくReduxストアに登録する手間は増えるものの、アプリを構成するコンポーネント間での情報を同期させ、それらの間の結合を疎にすることができる。

スクリーンショット 2024-10-14 17.53.15.png

まとめ

  • レンダリングは、HTMLやXMLを構成するツリー状のDOM要素にREACTコンポーネントを追加することで、動的にReactコンテンツをUIに反映させるための仕組みである

  • Reduxは、共有したいデータ(State)とActionをStoreに登録しておき、ReducerがDispatchされたActionとStateをもとにStateの更新を行うことでコンポーネント間でデータの授受を行わなくても同期を取り、コンポーネント間の疎結合を実現する仕組みである

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?