はじめに
前回の続きで、今回はReact+Reduxアプリケーションのフロント側の実装の内容を書いていきます。
使ったもの
- Node(v12.15.0)
- react(v16.13.1)
- redux(v7.2.0)
- redux-form(v8.3.1)
- redux-thunk(v2.3.0)
- axios(v0.19.2)
- @material-ui/core(v4.9.11)
- @material-ui/icons(v4.9.1)
ディレクトリ構成
root/
├─public
│ index.html
└─src
│ index.js
│
├─actions
│ index.js
│
├─components
│ login.js
│ privateRoute.js
│ users.js
│
├─reducers
│ index.js
│ login.js
│ users.js
│
└─styles
style.css
作るもの
ログイン画面とトップページを作ります。ログイン画面でユーザーIDとパスワードをAPIサーバーにリクエストして、認証したらユーザー情報を取得してトップページにユーザーネームを表示する簡単な実装です。見た目は@material-uiを使って少し調整した程度です。
ログイン画面
トップページ
プロジェクト作成
create-react-appでReactのプロジェクトを作成して、必要なパッケージをインストールします。
create-react-app app
npm install --save
RouterとRedux関連の設定
[src/index.js]に基本となるRouterの設定とRedux関連の設定を記述します。
[redux-thunk]は非同期処理を行えるようになるライブラリです。
ログイン画面とトップページのコンポーネントをルートとして設定しています。
import React from "react";
import ReactDOM from "react-dom";
import { createStore, applyMiddleware } from "redux";
import { Provider } from "react-redux";
import thunk from "redux-thunk";
import { BrowserRouter, Route, Switch } from "react-router-dom";
import "./styles/style.css";
import reducer from "./reducers";
import Login from "./components/login";
import TopPage from "./components/users";
import * as serviceWorker from "./serviceWorker";
const enhancer = applyMiddleware(thunk);
const store = createStore(reducer, enhancer);
ReactDOM.render(
<Provider store={store}>
<BrowserRouter>
<Switch>
<Route path="/login" component={Login} />
<Route path="/top" component={TopPage} />
<Route path="/" component={Login} />
</Switch>
</BrowserRouter>
</Provider>,
document.getElementById("root")
);
serviceWorker.unregister();
ログイン画面を作る
フォーム部分には[redux-form]を使用しています。バリデーションとかを実装しやすいので使いました。
また、テキストフィールドやボタンは[material-ui]を使用しています。
import React, { Component } from "react";
import { connect } from "react-redux";
import { Field, reduxForm } from "redux-form";
import { postLogin } from "../actions";
import TextField from "@material-ui/core/TextField";
import Button from "@material-ui/core/Button";
class Login extends Component {
constructor(props) {
super(props);
this.onSubmit = this.onSubmit.bind(this);
}
componentDidMount() {
localStorage.removeItem("token");
}
renderField(field) {
const {input,label,type,meta: { touched, error },} = field;
return touched && error ? (
<TextField error type={type} label={label} defaultValue={label} helperText={error} fullWidth={true} {...input} />
) : (
<TextField label={label} type={type} fullWidth={true} {...input} />
);
}
async onSubmit(values) {
await this.props.postLogin(values);
if (this.props.login.isSuccess && this.props.login.token) {
localStorage.setItem("token", this.props.login.token);
this.props.history.push("/top");
} else {
console.log("Failure Login...");
}
}
render() {
const { handleSubmit, pristine, submitting, invalid } = this.props;
return (
<div className="login__container">
<h1>Login</h1>
<form onSubmit={handleSubmit(this.onSubmit)}>
<div className="login__textfield">
<Field label="USERID" name="userId" type="text" component={this.renderField} />
</div>
<div className="login__textfield">
<Field
label="PASSWORD" name="passWord" type="password" component={this.renderField} />
</div>
<div className="login__submit">
<Button type="submit" variant="contained" color="primary" disabled={pristine || submitting || invalid} >
LOGIN
</Button>
</div>
</form>
</div>
);
}
}
const validate = (values) => {
const errors = {};
if (!values.userId) errors.userId = "enter a userID please.";
if (!values.passWord) errors.passWord = "enter a password please.";
return errors;
};
const mapStateToProps = (state) => ({ login: state.login });
const mapDispatchToProps = { postLogin };
export default connect(
mapStateToProps,
mapDispatchToProps
)(reduxForm({ validate, form: "loginForm" })(Login));
ログインボタン押下時
[mapDispatchToProps]で返ってきた[postLogin]アクションを実行します。
認証に成功したらトークンをローカルストレージに保存し、トップ画面に遷移します。
(ローカルストレージは賛否ありますが、一旦使用しています。)
async onSubmit(values) {
await this.props.postLogin(values);
if (this.props.login.isSuccess && this.props.login.token) {
localStorage.setItem("token", this.props.login.token);
this.props.history.push("/top");
} else {
console.log("Failure Login...");
}
}
[postlogin]アクション作成
APIサーバーとの通信は[axios]を使用しています。
フォームから受け取ったIDとパスワードをAPIサーバーに送信します。
import axios from "axios";
const ROOT_URL = "http://localhost:5000/api/v1";
export const POST_LOGIN = "POST_LOGIN";
export const postLogin = (values) => async (dispatch) => {
const response = await axios.post(`${ROOT_URL}/login`, values);
dispatch({ type: POST_LOGIN, response });
};
[login]reducer作成
import { POST_LOGIN } from "../actions";
export const login = (events = {}, action) => {
switch (action.type) {
case POST_LOGIN:
return action.response.data;
default:
return events;
}
};
combinereducerで連結
[combineReducers]を使用してreducerを連結します。
[redux-form]もここで連結します。
import { combineReducers } from "redux";
import { reducer as form } from "redux-form";
import { login } from "./login";
export default combineReducers({
login,
form,
});
トップ画面を作る
画面遷移時にログインしているユーザーの名前を表示する簡単なものです。
[componentDidMount]で[mapDispatchToProps]で返ってきた[getusers]アクションを実行します。
その後[mapStateToProps]で返ってきたusers.nameを画面に表示します。
import React, { Component } from "react";
import { connect } from "react-redux";
import { getUsers } from "../actions";
import { Link } from "react-router-dom";
class TopPage extends Component {
componentDidMount() {
this.props.getUsers();
}
render() {
return (
<React.Fragment>
<div className="toppage__container">
<h1>TopPage</h1>
<p className="toppage__message">Hello {this.props.users.name}!!</p>
<div className="toppage__navigation">
<ul>
<li><Link to="/top">Top</Link></li>
<li><Link to="/top">DashBoard</Link></li>
<li><Link to="/top">Profile</Link></li>
<li><Link to="/top">Contact</Link></li>
</ul>
</div>
</div>
</React.Fragment>
);
}
}
const mapStateToProps = (state) => {
return {
users: state.users,
};
};
const mapDispatchToProps = { getUsers };
export default connect(mapStateToProps, mapDispatchToProps)(TopPage);
[getUsers]アクション作成
トークンをリクエストヘッダーに付与(Authorization: Bearer token のように)して、GETリクエストしてユーザー情報を取得します。
src/actions/index.jsに以下追記してください。
export const GET_USERS = "GET_USERS";
export const getUsers = () => async (dispatch) => {
const token = localStorage.getItem("token");
const response = await axios.get(`${ROOT_URL}/users`, {
headers: { Authorization: `Bearer ${token}` },
data: {},
});
dispatch({ type: GET_USERS, response });
};
[users]reducer作成
import { GET_USERS } from "../actions";
export const users = (events = {}, action) => {
switch (action.type) {
case GET_USERS:
return action.response.data;
default:
return events;
}
};
combinereducerで連結
src/reducers/index.jsに[users]reducer追記してください。
import { combineReducers } from "redux";
import { reducer as form } from "redux-form";
import { login } from "./login";
import { users } from "./users";
export default combineReducers({
login,
users,
form,
});
まとめ
これでJWTによるログインとトップページでのユーザー情報取得の実装が終わりです。
ルートを保護する処理で完成となりますが、ちょっと長いので次回にしたいと思います。
何かまずいところあればコメントお願いします。