こんにちは、KOUKIです。Reactで画面開発を行なっています。
前回は、Profileページの実装を行いました。今回は、Reduxを導入します。
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
前回
事前準備
フォルダ/ファイル
1 2 3 4 5 |
mkdir -p react-admin/src/redux/actions mkdir react-admin/src/redux/reducers touch react-admin/src/redux/actions/setUserAction.ts touch react-admin/src/redux/reducers/setUserReducer.ts touch react-admin/src/redux/configureStore.ts |
モジュールのインストール
1 2 3 4 5 |
cd react-admin/ npm i react-redux npm i -D @types/react-redux npm i redux npm i -D @types/redux |
package.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 |
{ "name": "react-admin", "version": "0.1.0", "private": true, "dependencies": { "@material-ui/core": "^4.11.4", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.15", "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "axios": "^0.21.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "redux": "^4.1.0", "typescript": "^4.1.2", "web-vitals": "^1.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.1.7", "@types/redux": "^3.6.0" } } |
Reduxの導入
Reduxは、Reactアプリの状態を管理しやすくするためのフレームワークです。
例えば、本アプリでは、たくさんのPageやコンポーネントが存在します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
. ├── src │ ├── App.css │ ├── App.test.tsx │ ├── App.tsx │ ├── Login.css │ ├── components │ │ ├── Layout.tsx │ │ ├── Menu.tsx │ │ ├── Nav.tsx │ │ └── RedirectToUsers.tsx │ ├── index.css │ ├── index.tsx │ ├── logo.svg │ ├── models │ │ ├── Product.ts │ │ ├── link.ts │ │ ├── order-item.ts │ │ ├── order.ts │ │ └── user.ts │ ├── pages │ │ ├── Links.tsx │ │ ├── Login.tsx │ │ ├── Orders.tsx │ │ ├── Profile.tsx │ │ ├── Register.tsx │ │ ├── Users.tsx │ │ └── products │ │ ├── ProductForm.tsx │ │ └── Products.tsx │ ├── react-app-env.d.ts │ ├── redux │ │ ├── actions │ │ │ └── setUserAction.ts │ │ ├── configureStore.ts │ │ └── reducers │ │ └── setUserReducer.ts │ ├── reportWebVitals.ts │ └── setupTests.ts ├── tsconfig.json └── yarn.lock |
これらの間では、Propsなどを使ってデータの受け渡しを行い、状態管理を行なっていました。
しかし、開発規模が大きくなるとアプリケーションの状態を管理することが難しくなります。
「このデータはどこで変更してるんだっけ?」、「あれ、なんか変なデータが渡されている…!」みたいな感じになり、膨大なコードの海を漂って、頭をウンウンと捻らすことになります。
そこで、Reduxを使って、状態を一元管理できるようにしましょう。
Reduxを理解するには、次の3つのキーワードを知ることが必要です。
- State
- Action
- Reducer
Reactのコンポーネントは、内部に状態を持つことができます。これをStateと呼びます。Propsは、親のコンポーネントから値を渡されるものですが、 Stateはそのコンポーネント内部からのみ使用されます。Propsは、不変の値、Stateは可変の値です。
Actionは、アプリケーションの中で何が起きたのかを管理するためのものです。どんなアクションなのかを示すtypeとそれに紐づくユニークな値を持ちます。
Reducerは、actionが発動したとき、そのactionに組み込まれているtypeに応じて、状態(state)をどう変化させるのかを定義します。
Actionの実装
最初に、Actionを実装しましょう。
1 2 3 4 5 6 7 |
// redux/actions/setUserAction.ts import { UserProps as User } from '../../models/user' export const setUserAction = (user: User) => ({ type: 'SET_USER', user }) |
「SET_USER」というのが、typeであり、アクションです。「ユーザーをセットしますよ」という意味です。
型として使用しているUserProps(User)は、以前実装したInterfaceをClassに変換したものです。
1 2 3 4 5 6 7 8 9 10 |
// models/user.ts ... // interfeace -> classに変更 export class UserProps { id!: number first_name!: string last_name!: string email!: string } |
「Props」が付いているのがいけていませんが、ひとまず、このまま実装します。
Reducer
次に、Reducerを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// redux/reducers/setUserReducer.ts import { UserProps } from "../../models/user" import { setUserAction } from "../actions/setUserAction" const initialState = { user: new UserProps() } export const setUserReducer = ( state = initialState, action: {type: string, user: UserProps}) => { switch (action.type) { case 'SET_USER': return { ...state, user: action.user } default: return state } } |
initialStateは、その名の通り、Userデータの初期値です。
APIからデータを取得した時は、SET_USERアクションを使って、User情報をStateに持たせることになります。
Configureの作成
自作のReducerは、reduxのcreateStore関数を使って、登録する必要があります。
1 2 3 4 5 |
// redux/configureStore.ts import { createStore } from 'redux' import { setUserReducer } from './reducers/setUserReducer' export const configureStore = () => createStore(setUserReducer) |
Providerの実装
最後に、先ほど作成したStoreをアプリケーション内で使用できるようにするために、Providerを利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// src/index.tsx ... import { configureStore } from './redux/configureStore' import { Provider } from 'react-redux'; axios.defaults.baseURL = 'http://localhost:8000/api/admin/' axios.defaults.withCredentials = true // store const store = configureStore() ReactDOM.render( <React.StrictMode> {/* storeをコンポーネントに渡すため、Providerを定義 */} <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); reportWebVitals(); |
LayoutコンポーネントをReduxに登録
LayoutコンポーネントをRedux(Store)に登録するため、Connect処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
// components/Layout.tsx import { Dispatch, useEffect, useState } from 'react' ... import { connect } from 'react-redux' import { UserProps } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' i... const Layout = (props: any) => { ... } // State const mapStateToProps = (state: {user: UserProps}) => ({ user: state.user }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ // setUser(Action)->dispatch setUser: (user: UserProps) => dispatch(setUserAction(user)) }) // LayoutコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(Layout) |
connecに、StateとDispatchを登録することで、以下のaliasが登録されるようです。
1 2 3 4 5 6 7 8 9 10 11 12 |
(alias) connect<{ user: UserProps; }, { setUser: (user: UserProps) => void; }, {}, { user: UserProps; }>(mapStateToProps: MapStateToPropsParam<{ user: UserProps; }, {}, { user: UserProps; }>, mapDispatchToProps: MapDispatchToPropsNonObject<...>): InferableComponentEnhancerWithProps<...> (+14 overloads) import connect |
Layoutは、全てのコンポーネントを囲っている親コンポーネントです。ここに、StateとDispatchの初期値を定義することで、子コンポーネントにPropsを渡せるようになります。
例えば、NavコンポーネントをRedux Storeに登録し、Propsを渡す例を以下に記載します。
1 2 3 4 5 6 7 8 9 10 11 12 |
// src/components/Nav.tsx import { connect } from 'react-redux' ... // propsを追加 const Nav = (props: { user: UserProps | null }) => { ... } export default connect( (state: {user: UserProps}) => ({ user: state.user }))(Nav) |
このようにすると、User情報をRedux Storeから取り出すようになるので、LayoutからPropsを渡す必要がなくなります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// components/Layout.tsx ... const Layout = (props: any) => { ... useEffect(() => { ( async () => { try { ... // setUser(data) 不要 props.setUser(data) } catch(e) { setRedirect(true) } } )() }, []) ... return ( <div> {/* <Nav user={user}/> */ userを渡す必要がない} <Nav /> .... ) } |
ちょっと難しいかもしれませんね^^;
実装に問題がないか確認するために、ブラウザから「http://localhost:3000/」へアクセスし、User名が表示されているか確認します。

ページのナビゲーションに、「test test」が表示されているので、OKですね。
Profileコンポーネントのリファクタリング
ProfileコンポーネントもRedux Storeにコネクトしましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// pages/Profile.tsx import { connect } from 'react-redux' import { UserProps } from '../models/user' ... const Profile = () => { ... const userUrl = 'user' const userInfo = 'info' const userPassword = 'password' useEffect(() => { ( async () => { const { data } = await axios.get(userUrl) setFirstName(data.first_name) setLastName(data.last_name) setEmail(data.email) } )() }, []) ... } export default connect( (state: {user: UserProps}) => ({ user: state.user }))(Profile) |
Profileコンポーネントは、User APIへユーザー情報を取得し(useEffect)、その値をFormの値として設定していました。
しかし、Layoutコンポーネントがユーザー情報を取得し、Redux Storeを介して子コンポーネントに伝搬することが可能になったので、APIへのアクセスは不要になります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// pages/Profile.tsx // Props追加 const Profile = (props: any) => { ... const userInfo = 'info' const userPassword = 'password' // propsからデータを設定するのでAPIからデータを取得する必要はなくなる useEffect(() => { console.log(props.user) setFirstName(props.user.first_name) setLastName(props.user.last_name) setEmail(props.user.email) }, [props.user]) // propsを渡す ... } |
ブラウザから「http://localhost:3000/profile」へアクセスし、挙動確認をしてみましょう。

「First Name」、「Last Name」、「Email」それぞれの項目に値が入っているので、問題なく処理ができています 。
Profile更新不具合の修正
先ほどリファクタリングしたProfileコンポーネントには、一つ問題があります。
ユーザー情報を更新し、「SUBMIT」ボタンを押下しても、ユーザー名の表示が変わらないのです。
例えば、First Nameを「test2」にして、「SUBMIT」ボタンを押下してみます。

上記の画像は、SUBMIT後なのですが、ナビゲーションにあるUser名に変更はありません。
もちろん、画面を読み込み直すと更新されます。

この不具合を修正したいと思います。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// pages/Profile.tsx import { Dispatch, SyntheticEvent, useEffect, useState } from 'react' import { connect } from 'react-redux' ... // Props追加 const Profile = (props: any) => { ... const infoSubmit = async(e: SyntheticEvent) => { e.preventDefault() const { data } = await axios.put(userInfo, { first_name, last_name, email }) // dispatchを実行 props.setUser(data) } ... } export default connect( (state: {user: UserProps}) => ({ user: state.user }), // dispatchを追加 (dispatch: Dispatch<any>) => ({ setUser: (user: UserProps) => dispatch(setUserAction(user)) }) )(Profile) |
connectにdispatchを追加して、infoSubmit関数でAPIを呼び出し後にdispatchを実行しました。これで、Stateの状態が変わるので、画面が更新されるはずです。
OKですね。
次回
Adminページの実装は、以上です。
次回から、Ambassadorページの実装に入ります。
Reactまとめ
参考書籍
ソースコード
ここまで実装したソースコードを下記に記載します。
setUserAction.ts
1 2 3 4 5 6 7 |
// redux/actions/setUserAction.ts import { UserProps as User } from '../../models/user' export const setUserAction = (user: User) => ({ type: 'SET_USER', user }) |
setUserReducer.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// redux/reducers/setUserReducer.ts import { UserProps } from "../../models/user" import { setUserAction } from "../actions/setUserAction" const initialState = { user: new UserProps() } export const setUserReducer = ( state = initialState, action: {type: string, user: UserProps}) => { switch (action.type) { case 'SET_USER': return { ...state, user: action.user } default: return state } } |
configureStore.ts
1 2 3 4 5 |
// redux/configureStore.ts import { createStore } from 'redux' import { setUserReducer } from './reducers/setUserReducer' export const configureStore = () => createStore(setUserReducer) |
src/index.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import reportWebVitals from './reportWebVitals'; import axios from 'axios' import { configureStore } from './redux/configureStore' import { Provider } from 'react-redux'; axios.defaults.baseURL = 'http://localhost:8000/api/admin/' axios.defaults.withCredentials = true // store const store = configureStore() ReactDOM.render( <React.StrictMode> {/* storeをコンポーネントに渡すため、Providerを定義 */} <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); reportWebVitals(); |
user.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// models/user.ts export class User { first_name: string last_name: string email: string password: string password_confirm: string constructor( first_name: string, last_name: string, email: string, password: string, password_confirm: string) { this.first_name = first_name this.last_name = last_name this.email = email this.password = password this.password_confirm = password_confirm } } // interfeace -> classに変更 export class UserProps { id!: number first_name!: string last_name!: string email!: string } |
Profile.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
// pages/Profile.tsx import { Dispatch, SyntheticEvent, useEffect, useState } from 'react' import { connect } from 'react-redux' import { UserProps } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' import { Button, TextField } from '@material-ui/core' import Layout from '../components/Layout' import axios from 'axios' // Props追加 const Profile = (props: any) => { const [first_name, setFirstName] = useState("") const [last_name, setLastName] = useState("") const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [password_confirm, setPasswordConfirm] = useState("") const userInfo = 'info' const userPassword = 'password' // propsからデータを設定するのでAPIからデータを取得する必要はなくなる useEffect(() => { console.log(props.user) setFirstName(props.user.first_name) setLastName(props.user.last_name) setEmail(props.user.email) }, [props.user]) // propsを渡す const infoSubmit = async(e: SyntheticEvent) => { e.preventDefault() const { data } = await axios.put(userInfo, { first_name, last_name, email }) // dispatchを実行 props.setUser(data) } const passwordSubmit = async(e: SyntheticEvent) => { e.preventDefault() await axios.put(userPassword, { password, password_confirm }) } return ( <Layout> <h3>Account Information</h3> <form onSubmit={infoSubmit}> <div className="mb-3"> <TextField label="First Name" onChange={e => setFirstName(e.target.value)} value={first_name} /> </div> <div className="mb-3"> <TextField label="Last Name" onChange={e => setLastName(e.target.value)} value={last_name} /> </div> <div className="mb-3"> <TextField label="Email" onChange={e => setEmail(e.target.value)} value={email} /> </div> <Button variant="contained" color="primary" type="submit"> Submit </Button> </form> <h3 className="mt-4">Change Password</h3> <form onSubmit={passwordSubmit}> <div className="mb-3"> <TextField label="Password" type="password" onChange={e => setPassword(e.target.value)} /> </div> <div className="mb-3"> <TextField label="Password Confirm" type="password" onChange={e => setPasswordConfirm(e.target.value)} /> </div> <Button variant="contained" color="primary" type="submit"> Submit </Button> </form> </Layout> ) } export default connect( (state: {user: UserProps}) => ({ user: state.user }), // dispatchを追加 (dispatch: Dispatch<any>) => ({ setUser: (user: UserProps) => dispatch(setUserAction(user)) }) )(Profile) |
Layout.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 |
// components/Layout.tsx import { Dispatch, useEffect, useState } from 'react' import { Redirect } from 'react-router-dom' import { connect } from 'react-redux' import { UserProps } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' import Nav from '../components/Nav' import Menu from '../components/Menu' import axios from 'axios' const Layout = (props: any) => { const userURL = 'user' const [redirect, setRedirect] = useState(false) // User情報のState const [user, setUser] = useState<UserProps | null>(null) useEffect(() => { ( async () => { try { const { data } = await axios.get(userURL) props.setUser(data) } catch(e) { setRedirect(true) } } )() }, []) if (redirect) { // ログイン画面へリダイレクト return <Redirect to={'/login'} /> } return ( <div> <Nav /> <div className="container-fluid"> <div className="row"> <Menu /> <main className="col-md-9 ms-sm-auto col-lg-10 px-md-4"> <div className="table-responsive"> {props.children} </div> </main> </div> </div> </div> ) } // State const mapStateToProps = (state: {user: UserProps}) => ({ user: state.user }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ // setUser(Action)->dispatch setUser: (user: UserProps) => dispatch(setUserAction(user)) }) // LayoutコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(Layout) |
Nav.tsx
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// src/components/Nav.tsx import { connect } from 'react-redux' import { Link } from 'react-router-dom' import { UserProps } from '../models/user' import axios from 'axios' // propsを追加 const Nav = (props: { user: UserProps | null }) => { const logout = async () => { await axios.post('logout') } return ( <header className="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow"> <a className="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">Company name</a> <div className="navbar-nav"> <div className="nav-item text-nowrap"> <Link to={'/profile'} className="nav-link px-3" > {props.user?.first_name} {props.user?.last_name} </Link> <Link to={'/login'} className="nav-link px-3" onClick={logout} > Sign out </Link> </div> </div> </header> ) } export default connect( (state: {user: UserProps}) => ({ user: state.user }))(Nav) |
コメントを残す
コメントを投稿するにはログインしてください。