Reduxのデータフロー 解説(1/2)
はじめに
Reduxの公式ドキュメントに沿って概要を押さえつつ、気になった部分を深掘って整理しています。
本記事では 、Reduxのサンプルアプリを通してデータフローについてまとめていきます。
アプリの構成
構成はこのようになっています。
├── /public # HTMLホストページのテンプレートやアイコンなどの静的ファイル │ ├── /src │ ├── index.js # アプリケーションのエントリーポイント。<Provider>と<App>コンポーネントをレンダリングする │ ├── App.js # メインのアプリケーションコンポーネント。ナビゲーションバーをレンダリングし、ルーティングを処理する │ └── index.css # アプリケーション全体のスタイル │ ├── /src/api │ ├── client.js # GETとPOSTリクエストを可能にする小さなAJAXリクエストクライアント │ └── server.js # データのフェイクREST APIを提供する。アプリはこれらのフェイクエンドポイントからデータを取得する │ └── /src/app ├── Navbar.js # 上部のヘッダーとナビゲーションコンテンツをレンダリングする └── store.js # Reduxストアのインスタンスを作成する
https://codesandbox.io/s/github/reduxjs/redux-essentials-example-app/tree/master/?from-embed
上記のアプリをロードすると、ヘッダーとウェルカムメッセージが表示されるはずです。また、Redux DevTools Extensionを開いて、Reduxの初期状態が完全に空であることを確認できます。
スライスの作成
最初のステップは、投稿のデータを格納する新しいReduxの "スライス "を作成を行います。
Reduxストアにデータを格納したら、そのデータをページに表示するためのReactコンポーネントを作成します。
srcの中に新しいfeaturesフォルダを作り、featuresの中にpostsフォルダを置き、postsSlice.jsという新しいファイルを追加します。
Redux ToolkitのcreateSlice関数を使って、投稿データの処理方法を知っているreducer関数を作ります。
アプリの起動時にReduxストアにこれらの値が読み込まれるように、リデューサー関数には初期データが含まれている必要があります。
今回の例ではダミーの投稿オブジェクトを含む配列を作成しています。
createSliceをインポートして、初期posts配列を定義し、それをcreateSliceに渡して、createSliceが生成してくれたposts reducer関数をエクスポートします。
features/posts/postsSlice.js
import { createSlice } from '@reduxjs/toolkit' const initialState = [ { id: '1', title: 'First Post!', content: 'Hello!' }, { id: '2', title: 'Second Post', content: 'More text' } ] const postsSlice = createSlice({ name: 'posts', initialState, reducers: {} }) export default postsSlice.reducer
ストアに作成したスライスを追加する
新しいスライスを作成した場合、そのリデューサー関数をReduxストアに追加しないといけません。
**app/store.js**
でpostsReducer関数をインポートし、configureStoreの呼び出しを更新して、postsReducerがpostsという名前のreducerフィールドとして渡されるようにします。
import { configureStore } from '@reduxjs/toolkit' import postsReducer from '../features/posts/postsSlice' export default configureStore({ reducer: { posts: postsReducer } })
このようにトップレベルのステートオブジェクトにpostsというフィールドを持たせることで、
アクションがディスパッチされたときにpostsReducer関数を使ってstate.postsのデータが更新されるようにしたいことをReduxに伝えることができます。
リスト表示
ストアに投稿データができたので、投稿リストを表示するReactコンポーネントを作成します。
投稿機能に関連するコードはすべてpostsフォルダにあるので、そこにPostsList.jsという名前のファイルを新規作成します。
投稿のリストをレンダリングするには、データをどこからか取得する必要があります。
Reactコンポーネントは、useSelectorフックを使ってReduxストアからデータを読み込むことができます。
定義した「セレクタ関数」は、Reduxのステートオブジェクト全体をパラメータとして呼び出され、このコンポーネントがストアから必要とする特定のデータを返すことができます。
最初のPostsListコンポーネントは、Reduxストアからstate.postsの値を読み込み、投稿の配列をループして、それぞれの投稿を画面に表示しています。
features/posts/PostsList.js
import React from 'react' import { useSelector } from 'react-redux' export const PostsList = () => { const posts = useSelector(state => state.posts) const renderedPosts = posts.map(post => ( <article className="post-excerpt" key={post.id}> <h3>{post.title}</h3> <p className="post-content">{post.content.substring(0, 100)}</p> </article> )) return ( <section className="posts-list"> <h2>Posts</h2> {renderedPosts} </section> ) }
コンポーネントの流用 - リスト
今回の例では先程作成した投稿リストをトップのコンポーネントで表示するようにします。
メインページにフォームを追加する予定なので、React Fragmentでラップします。
App.js
import React from 'react' import { BrowserRouter as Router, Switch, Route, Redirect } from 'react-router-dom' import { Navbar } from './app/Navbar' import { PostsList } from './features/posts/PostsList'// ーーーー追加 function App() { return ( <Router> <Navbar /> <div className="App"> <Switch> <Route exact path="/" render={() => ( <React.Fragment> <PostsList /> // ーーーーーーーーーーーーーーーーーーーー追加 </React.Fragment> )} /> <Redirect to="/" /> </Switch> </div> </Router> ) } export default App
Fragmentの必要有無について
Reactのフラグメントは、React要素のグループを返す必要があるが、余分なノードをDOMに追加したくないときに役立ちます。
複数の子要素を返すコンポーネントでは必要
Reactのコンポーネントでは、通常、1つの親要素だけを返す必要があります。
以下のようなコードは不適切です。
function Example() { return ( <div>First Element</div> <div>Second Element</div> ) }
上記のコードは、2つの要素が同じレベルにあり、それらを1つの親要素でラップしていないため、エラーとなります。
フラグメントを使うことで問題を解決できます。
function Example() { return ( <> <div>First Element</div> <div>Second Element</div> </> ) }
単一の要素を返す場合は不要
コンポーネントが1つの要素だけを返す場合は、フラグメントは必要ありません。
function Example() { return <div>Single Element</div> }
余分なDOM要素が問題にならない場合は、divタグでも代用可
フラグメントの主な目的は余分なDOM要素を防ぐことです。
余分なDOM要素が特に問題にならない場合にはフラグメントは必要ありません。
function Example() { return ( <div> <div>First Element</div> <div>Second Element</div> </div> ) }
少し脱線しました。
サンプル
先程のサンプルコードを適用させると下記の画面になるはずです。
参考
続く…
コメント
本記事の内容は以上になります!
書籍の続きのアウトプットも随時更新したいと思います。
プログラミングスクールのご紹介 (卒業生より)
お世話になったプログラミングスクールであるRUNTEQです♪
こちらのリンクを経由すると1万円引きになります。
RUNTEQを通じて開発学習の末、受託開発企業をご紹介いただき、現在も双方とご縁があります。
もし、興味がありましたらお気軽にコメントか、TwitterのDMでお声掛けください。