サイトアイコン 上尾市のWEBプログラマーによるブログ

「ReactでReduxを使ってみよう」の感想・備忘録2

「ReactでReduxを使ってみよう」の感想・忘備録1の続き

非同期処理

Redux Thunk

Reduxを使ったアプリでHTTP通信などの非同期処理を行う場合はRedux Thunkを使用する。
Redux ThunkはReduxのミドルウェアの1つで、非同期処理の結果によってdispatchするActionを変更することができる。

  1. npx create-react-app asyncapp
  2. cd asyncapp; npm install redux react-redux redux-thunk
  3. cd ./src; mkdir reducers; touch ./reducers/counterReducer.js
const counterReducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1
    case 'DECREMENT':
      return state - 1
    default:
      return state
  }
};
export default counterReducer;
  1. mkdir actions; touch ./actions/counterActions.js
export const increment = () => {
  return dispatch => {
    setTimeout(() => {
      dispatch({ type: 'INCREMENT' });
    }, 2000);
  }
};
export const decrement = () => {
  return { type: 'DECREMENT' };
};

Redux ThunkではActionCreatorで返すActionをオブジェクトではなく関数にすることができる。
関数の中で非同期処理を行い、結果によって複数のActionオブジェクトをdispatchする。

  1. index.jsを修正
// 〜省略〜 //
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import counterReducer from './reducers/counterReducer';
import thunk from 'redux-thunk';
const store = createStore(counterReducer, applyMiddleware(thunk));

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
serviceWorker.unregister();

Reduxのミドルウェアを使う場合は、createStoreの第2引数にapplyMiddlewareの戻り値を渡す。
const store = createStore(counterReducer, applyMiddleware(thunk));

  1. App.jsを修正
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increment, decrement } from './actions/counterActions';

class App extends Component {
  render() {
    return (
      <div>
        <p>{this.props.count}回クリックされました。</p>
        <button onClick={this.props.increment}>+</button>
        <button onClick={this.props.decrement}>-</button>
      </div>
    );
  }
}
const mapStateToProps = (state) => {
  return {
    count: state
  }
};
const mapDispatchToProps = (dispatch) => {
  return {
    increment: () => dispatch(increment()),
    decrement: () => dispatch(decrement())
  }
};
export default connect(mapStateToProps, mapDispatchToProps)(App);
  1. npm startで実行

実践

階層構造を持つアプリのRedux化

Reactで作成されたTodoアプリをRedux化する。

Appコンポーネントの中にTodoListコンポーネントがあり、その中にTodoItemコンポーネントがある場合、データとデータを変更するメソッドをAppで定義し、App⇒TodoList⇒TodoItemと渡す必要がある。
逆にTodoItemでのデータの変更はTodoItem⇒TodoList⇒Appと伝える必要がある。

Reduxを使うと、各コンポーネントは直接Redux Storeにアクセスすることができる。

reducers/todoReducers.js

const todoReducer = (state = {todos: []}, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      return {todos: [...state.todos, action.text]};
    case 'DELETE_TODO':
      const todos = [...state.todos];
      todos.splice(action.index, 1);
      return {todos};
    default:
      return state
  }
};
export default todoReducer;

actions/todoActions.js

export const addTodo = (text) => {
  return { type: 'ADD_TODO', text };
};
export const deleteTodo = (index) => {
  return { type: 'DELETE_TODO', index};
};

index.jsにreduxの記述を追加

import { createStore } from 'redux';
import { Provider } from 'react-redux';
import todoReducer from './reducers/todoReducer';
const store = createStore(todoReducer);
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

components/TodoForm.jsにconnectを追加


import { connect } from 'react-redux';
import { addTodo } from '../actions/todoActions';
// 〜省略〜
const mapDispatchToProps = (dispatch) => {
  return {
    addTodo: (text) => dispatch(addTodo(text)),
  }
};
export default connect(null, mapDispatchToProps)(TodoForm);

components/TodoList.jsにconnectを追加

import { connect } from 'react-redux';
// 〜省略〜
const mapStateToProps = (state) => {
  return {
    todos: state.todos
  }
};
export default connect(mapStateToProps)(TodoList);

components/TodoItem.jsにconnectを追加

import { connect } from 'react-redux';
import { deleteTodo } from '../actions/todoActions';
// 〜省略〜
const mapDispatchToProps = (dispatch) => {
  return {
    deleteTodo: (index) => dispatch(deleteTodo(index)),
  }
};
export default connect(null, mapDispatchToProps)(TodoItem);

App.js

App.jsではコンポーネントにpropsへ値は渡さず、データの保持やデータを更新するメソッドの定義も不要。

return (
  <div>
    <TodoForm/>
    <TodoList/>
  </div>
);

HTTP通信を行うアプリのRedux化

Redux化前

  1. create-react-app userapp
  2. cd userapp; npm install react-router-dom
  3. cd ./src; mkdir components; touch ./components/UserList.js
  4. touch ./components/UserDetail.js
import UserList from './components/UserList';
import UserDetail from './components/UserDetail';
// 〜省略〜
<BrowserRouter> 
  <Route path="/" component={UserList} exact/>
</BrowserRouter>
componentDidMount() {
  fetch('https://jsonplaceholder.typicode.com/users')
    .then((res) => res.json())
    .then((data) => {
      this.setState({users: data});
    })
    .catch((err) => console.log(err));
}
viewUser = (e) => {
  this.props.history.push(`/view/${e.target.dataset.index}/`);
};
// 〜省略〜
  {
    this.state.users.map((user) => {
      return (
        <li key={user.id}>
          {user.name}
          <button onClick={this.viewUser} data-index={user.id}>詳細</button>
        </li>
      );
    })
  }
import { Link } from 'react-router-dom';
// 〜省略〜
constructor(props) {
  super(props);
  this.state = {
    name: '',
    email: ''
  }
}
componentDidMount() {
  fetch(`https://jsonplaceholder.typicode.com/users/${this.props.match.params.id}`)
    .then((res) => res.json())
    .then((data) => {
      this.setState({name: data.name, email: data.email});
    })
    .catch((err) => console.log(err));
}
// 〜省略〜
<Link to="/">一覧へ戻る</Link>
<p>{this.state.name}:{this.state.email}</p>

Redux化後

  1. npm install redux react-redux redux-thunk
  2. cd ./src; mkdir reducers; touch ./reducers/UserReducers.js
const userReducer = (state = { fetching: false, fetched: false, users: [], error: null }, action) => {
  switch (action.type) {
    case 'FETCH_USER_START':
      return {...state, fetching: true};
    case 'FETCH_USER_SUCCESS':
      return {...state, fetching: false, fetched: true, users: action.users};
    case 'FETCH_USER_ERROR':
      return {...state, fetching: false, error: action.error};
    default:
      return state
  }
};
export default userReducer;
  1. mkdir actions; touch ./actions/userActions.js
export const fetchUser = () => {
  return dispatch => {
    dispatch({type: 'FETCH_USER_START'});
    fetch('https://jsonplaceholder.typicode.com/users')
      .then((res) => res.json())
      .then((data) => {
        dispatch({type: 'FETCH_USER_SUCCESS', users: data});
      })
      .catch((err) => {
        dispatch({type: 'FETCH_USER_ERROR', error: err});
      });
  };
};
  1. mkdir actions; touch ./actions/userActions.js
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import userReducer from './reducers/userReducer';
import thunk from 'redux-thunk';
const store = createStore(userReducer, applyMiddleware(thunk));
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);
  1. UserList.jsにconnectを追加
import React from "react";
import { connect } from 'react-redux';
import { fetchUser } from '../actions/userActions';

class UserList extends React.Component {
  componentDidMount() {
    this.props.fetchUser();
  }
// 〜省略〜
this.props.users.map((user) => {
  return (
    <li key={user.id}>{user.name}<button onClick={this.viewUser} data-index={user.id}>詳細</button></li>
  );
})
// 〜省略〜
const mapStateToProps = (state) => {
  return { users: state.users };
};
const mapDispatchToProps = (dispatch) => {
  return { fetchUser: (text) => dispatch(fetchUser()) }
};
export default connect(mapDispatchToProps, mapDispatchToProps)(UserList);
  1. UserDetail.jsにconnectを追加
import React from "react";
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
// 〜省略〜
componentDidMount() {
  const user = this.props.users.find(user => user.id === Number(this.props.match.params.id));
    this.setState({name: user.name, email: user.email});
}
// 〜省略〜
const mapStateToProps = (state) => {
  return {
    users: state.users
  }
};
export default connect(mapStateToProps)(UserDetail);

JWT(JSON Web Token)とは

REST APIのアクセスを制限するときに使用するトークンを実装する方法の1つ。

  1. ログイン成功時にサーバはJWTを返す。
  2. 以降は、Authorization Headerに「Bearer JWTトークン」を付けてアクセスする。

node.jsにはjsonwebtokenというnpmがある。

jsonwebtokenの使い方

  1. npm install jsonwebtokenでインストール。
  2. 以下のソースをcreate_jwt.jsとして保存。
  3. node create_jwt.jsで実行
const jwt = require('jsonwebtoken');

// JWTの生成
// 同期処理の場合
const token = jwt.sign({id: '100'}, 'himitsu123', { expiresIn: 60 });
console.log(token);
// 非同期処理の場合
jwt.sign({id: '100'}, 'himitsu123', { expiresIn: 60 }, function(err, token) {
  console.log(token);
});
// JWTの検証
try {
  const decoded = jwt.verify(token, 'himitsu123');
  console.log(decoded); // { id: '100', iat: 1600461262, exp: 1600461322 } iat=issued at=発行日時
} catch (err) {
  console.log(err);
}

JWTを利用した認証を行うアプリのRedux化

ログイン成功時に取得したtokenをLocalStrageに保存し、認証が必要なアクセスにはtokenをAuthorizationヘッダーにBearerスキームで付ける。
これをReduxを使って書き換えると、LocalStrageではなくRedux Storeに保存するようになる。

手順やソースは省略。

Bearerスキームとは

HTTP 認証スキームの一つで、WebAPI へアクセスするためのセキュリティトークン(アクセストークン等)をAuthorization: Bearer {トークン}という形で HTTP ヘッダーにセットし、トークンの受け渡しを行う仕組み。

ログイン状態により表示するコンポーネントを切り替える

https://qiita.com/ginban22/items/a36d01b41deaeedd581e https://reactrouter.com/web/example/auth-workflow

connected-react-router

react-reduxのconnectを使ったコンポーネント内で、ルーティング情報の取得やpushメソッドなどが使えるようになる。
https://qiita.com/hiroya8649/items/34979f2008cf92c110ff

モバイルバージョンを終了