非同期処理
Redux Thunk
Reduxを使ったアプリでHTTP通信などの非同期処理を行う場合はRedux Thunkを使用する。
Redux ThunkはReduxのミドルウェアの1つで、非同期処理の結果によってdispatchするActionを変更することができる。
npx create-react-app asyncapp
cd asyncapp; npm install redux react-redux redux-thunk
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;
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する。
- 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));
- 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);
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化前
create-react-app userapp
cd userapp; npm install react-router-dom
cd ./src; mkdir components; touch ./components/UserList.js
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化後
npm install redux react-redux redux-thunk
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;
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});
});
};
};
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')
);
- 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);
- 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つ。
- ログイン成功時にサーバはJWTを返す。
- 以降は、Authorization Headerに「Bearer JWTトークン」を付けてアクセスする。
node.jsにはjsonwebtokenというnpmがある。
jsonwebtokenの使い方
npm install jsonwebtoken
でインストール。- 以下のソースをcreate_jwt.jsとして保存。
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