こんにちは、KOUKIです。
GolangとReactでMovie Appの開発をしています。
今回は、フロントエンド側(React)でJWT Tokenのハンドリング処理を実装します。
尚、Udemyの「Working with React and Go (Golang)」を参考にしているので、よかったら受講してみてください。
<目次>
前回
Login APIへリクエストを送信する処理を実装しました。
事前準備
フォルダ/ファイル
1 2 3 4 5 6 |
mkdir go-movies/src/redux mkdir go-movies/src/redux/actions mkdir go-movies/src/redux/reducers touch go-movies/src/redux/configureStore.ts touch go-movies/src/redux/actions/setJWTAction.ts touch go-movies/src/redux/reducers/setJWTReducer.ts |
モジュール
1 2 3 4 |
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": "go-movies", "version": "0.1.0", "private": true, "dependencies": { "@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-dom": "^17.0.0", "axios": "^0.21.1", "react": "^17.0.2", "react-confirm-alert": "^2.7.0", "react-dom": "^17.0.2", "react-redux": "^7.2.4", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "redux": "^4.1.1", "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": "^17.0.15", "@types/react-redux": "^7.1.18", "@types/react-router-dom": "^5.1.8", "@types/redux": "^3.6.0" } } |
JWT Tokenのハンドリング
ログインの仕組みですが、適切なユーザー情報をAPIへ送信後JWT Tokenを発行し、ログイン認証済みであることを証明します。
そして、ログアウトをすることでJWT Tokenを削除し、未ログインの状態に戻します。
つまり、JWT Tokenの取扱には十分気をつける必要があるということです。
ログイン成功時
ログイン成功時のJWT Tokenのハンドリングを実装しましょう。
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 |
// components/Login.tsx ... const Login = (props: any) => { ... const [jwt, setJWT] = useState("") const submit = async (e: SyntheticEvent) => { ... await axios.post("signin", JSON.stringify(payload)) .then((response) => { console.log(response.data.response) setJWT(response.data.response) props.history.push({ pathname: "/admin" }) }) .catch((err) => {... }) } return ( ... ) } export default Login |
ログイン成功時のレスポンスは、JWT TokenなのでuseStateで用意したjwt変数に格納しました。そして、admin画面に遷移するようにpropsのhistory.pushを追加しています。
実際にログインしてみましょう。


成功!と言いたいところですが、よくみると「login」ボタンのままだったり、ログインした時に表示される「Add Movie」、「Manage Catalog」メニューが表示されていません。
実は、現在の処理ではLoginコンポーネント内でしかJWT Tokenを保持しないので、別の方法を考える必要があります。
Reduxの導入
今回は、Reduxを導入して対処することにしましょう。
Reduxは、actionというイベントを使ってstateを状態を管理するReactのフレームワークです。
詳しい内容は下記の記事で解説しているので、よかったら一読してください。
Actionの実装
JWTをセットするActionを実装します。
1 2 3 4 5 |
// redux/actions/setJWTAction.ts export const setJWTAction = (jwt: string) => ({ type: 'SET_JWT', jwt }) |
Reducerの実装
Actionが実行されたら新しくStateを作成し、JWTを保存します。そのために、Reducerを実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// redux/reducers/setJWTReducer.ts const initialState = { jwt: '', } export const setJWTReducer = ( state = initialState, action: {type: string, jwt: string}) => { switch (action.type) { case 'SET_JWT': return {jwt: action.jwt} default: return state } } |
Configure
ReducerをRedux Storeに登録します。
1 2 3 4 5 6 |
// redux/configureStore.ts import { createStore } from 'redux' import { setJWTReducer } from './reducers/setJWTReducer' export const configureStore = () => createStore(setJWTReducer) |
Provider
先ほど登録したRedux Storeをアプリケーション内で利用できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// src/index.tsx ... import { configureStore } from './redux/configureStore' import { Provider } from 'react-redux'; // store const store = configureStore() axios.defaults.baseURL = 'http://localhost:4000/v1/' ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); |
AppコンポーネントをReduxに登録
AppコンポーネントにReduxを登録します。そのために、まず関数の作り方を変えます。
1 2 3 4 5 6 7 8 |
// src/App.tsx ... // export default function App() { const App = (props: any)) => { } export default Ap |
続いて、state及びdispatchの定義とstoreへの登録処理を実装します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// src/App.tsx import { Dispatch, Fragment, useState } from 'react' import { connect } from 'react-redux' import { setJWTAction } from './redux/actions/setJWTAction' ... <meta charset="utf-8">const App = (props: any)) => { ... } // State -> Props const mapStateToProps = (state: {jwt: string}) => ({ jwt: state.jwt }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) // AppコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(App) |
DispatchはStateを変更するときのトリガーになるものです。また、StateをPropsに変更し、子コンポーネントに値を伝搬できるようにしました。
LoginコンポーネントをReduxに登録
LoginコンポーネントもReduxに登録します。ここで重要になってくるのが、Dispatchです。
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 |
// components/Login.tsx import { connect } from 'react-redux' import { setJWTAction } from '../redux/actions/setJWTAction' ... const Login = (props: any) => { ... // const [jwt, setJWT] = useState("") const submit = async (e: SyntheticEvent) => { ... await axios.post("signin", JSON.stringify(payload)) .then((response) => { console.log(response.data.response) // setJWT(response.data.response) props.setJWT(response.data.response) // 実装したdispatchを呼び出して、storeにjwtを保存 props.history.push({ pathname: "/admin" }) }) ... }) } ... return ( ... ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Login) |
Appコンポーネントと同じようにRedux Storeに登録処理を行いました。またdispatchを登録しているので、setJWTをpropsから呼び出すことが可能となり、JWTをRedux Storeに登録することができるようになりました。
ログイン判定の変更
propsからJWTを取り出すことができるようになったので、Appコンポー円んとのログイン判定処理を変更します。
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 |
// src/App.tsx ... const App = (props: any) => { // const [jwt, setJWT] = useState("") // const handleJWTChange = (jwt: string) => { // setJWT(jwt) // } const logout = () => { // setJWT("") props.setJWT("") // dispatchを発動 } ... return ( <Router> ... <div className="row"> <div className="col-md-2"> <nav> ... { props.jwt !== "" && // propsからJWTを確認する <Fragment> ... </Fragment> } </ul> </nav> </div> <div className="col-md-10"> <Switch> ... {/* <Route exact path="/login" component={(props: any) => <Login {...props} handleJWTChange={handleJWTChange} />} /> */} <Route exact path="/login" component={(props: any) => <Login {...props} />} /> // handleJWTChangeは不要 ... </Switch> </div> </div> </div> </Router> ) } ... |
検証
それでは、検証をしてみましょう。
OKですね。ログインからログアウトまで機能することが確認できました。
Reduxの実装は少し複雑ですが、かなり便利ですね。
次回
記事まとめ
参考書籍
ソースコード
Login.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 |
// components/Login.tsx import { Dispatch, useState, Fragment, SyntheticEvent } from 'react' import { connect } from 'react-redux' import { AlertType } from '../models/ui-components' import { Credentials } from '../models/tokens' import { setJWTAction } from '../redux/actions/setJWTAction' import Alert from './ui-components/Alert' import Input from './form-components/Input' import axios from 'axios' const Login = (props: any) => { const [email, setEmail] = useState("") const [password, setPassword] = useState("") const [error, setError] = useState("") const [errors, setErrors] = useState<string[]>([]) const [alert, setAlert] = useState<AlertType>({type: "d-none", message: ""}) const submit = async (e: SyntheticEvent) => { e.preventDefault() // エラー初期化 setErrors([]) let submitErrors: string[] = [] if (email === "") { submitErrors.push("email") } if (password === "") { submitErrors.push("password") } if (submitErrors.length > 0) { setErrors(submitErrors) return } const payload: Credentials = { username: email, password: password } await axios.post("signin", JSON.stringify(payload)) .then((response) => { props.setJWT(response.data.response) props.history.push({ pathname: "/admin" }) }) .catch((err) => { setError(err.response.data.error.message) setAlert({ type: "alert-danger", message: err.response.data.error.message}) }) } const hasError = (key: string) => { return errors.indexOf(key) !== -1 } return ( <Fragment> <h2>Login</h2> <hr /> <Alert alertType={alert.type} alertMessage={alert.message} /> <form className="pt-3" onSubmit={submit}> <Input title={"Email"} type={'email'} name={'email'} handleChange={setEmail} className={hasError("email") ? "is-invalid" : ""} errorDiv={hasError("email") ? "text-danger" : "d-none"} errorMsg={"Please enter a valid email address"} /> <Input title={"Password"} type={'password'} name={'password'} handleChange={setPassword} className={hasError("password") ? "is-invalid" : ""} errorDiv={hasError("password") ? "text-danger" : "d-none"} errorMsg={"Please enter a password"} /> <hr /> <button className="btn btn-primary">Login</button> </form> </Fragment> ) } export default connect( (state: {jwt: string}) => ({ jwt: state.jwt }), (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) )(Login) |
tokens.ts
1 2 3 4 5 6 |
// models/tokens.ts export interface Credentials { username: string password: string } |
App.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 |
// src/App.tsx import { Dispatch, Fragment, useState } from 'react' import { BrowserRouter as Router, Switch, Route, Link } from 'react-router-dom' import { connect } from 'react-redux' import { setJWTAction } from './redux/actions/setJWTAction' import Home from './components/Home' import Movies from './components/Movies' import Admin from './components/Admin' import Genres from './components/Genres' import OneMovie from './components/OneMovie' import OneGenre from './components/OneGenre' import EditMovie from './components/EditMovie' import Login from './components/Login' const App = (props: any) => { const logout = () => { props.setJWT("") } let loginLink if (props.jwt === "") { loginLink = <Link to="/login">Login</Link> } else { loginLink = <Link to="/logout" onClick={logout}>Logout</Link> } return ( <Router> <div className="container"> <div className="row"> <div className="col mt-3"> <h1 className="mt-3"> Go Watch a Movie! </h1> </div> <div className="col mt-3 text-end"> {loginLink} </div> <hr className="mb-3" /> </div> <div className="row"> <div className="col-md-2"> <nav> <ul className="list-group"> <li className="list-group-item"> <Link to="/">Home</Link> </li> <li className="list-group-item"> <Link to="/movies">Movies</Link> </li> <li className="list-group-item"> <Link to="/genres">Genres</Link> </li> { props.jwt !== "" && <Fragment> <li className="list-group-item"> <Link to="/admin/movie/0">Add Movie</Link> </li> <li className="list-group-item"> <Link to="/admin">Manage Catalog</Link> </li> </Fragment> } </ul> </nav> </div> <div className="col-md-10"> <Switch> <Route path="/movies/:id" component={OneMovie} /> <Route path="/movies"> <Movies /> </Route> <Route path="/genre/:id" component={OneGenre} /> <Route exact path="/login" component={(props: any) => <Login {...props} />} /> <Route exact path="/genres"> <Genres /> </Route> <Route path="/admin/movie/:id" component={EditMovie} /> <Route path="/admin"> <Admin /> </Route> <Route path="/"> <Home /> </Route> </Switch> </div> </div> </div> </Router> ) } // State -> Props const mapStateToProps = (state: {jwt: string}) => ({ jwt: state.jwt }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ setJWT: (jwt: string) => dispatch(setJWTAction(jwt)) }) // AppコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(App) |
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 |
// src/index.tsx import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; import axios from 'axios' import { configureStore } from './redux/configureStore' import { Provider } from 'react-redux'; // store const store = configureStore() axios.defaults.baseURL = 'http://localhost:4000/v1/' ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); |
configureStore.ts
1 2 3 4 5 6 |
// redux/configureStore.ts import { createStore } from 'redux' import { setJWTReducer } from './reducers/setJWTReducer' export const configureStore = () => createStore(setJWTReducer) |
setJWTAction.ts
1 2 3 4 5 6 |
// redux/actions/setJWTAction.ts export const setJWTAction = (jwt: string) => ({ type: 'SET_JWT', jwt }) |
setJWTReducer.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// redux/reducers/setJWTReducer.ts const initialState = { jwt: '', } export const setJWTReducer = ( state = initialState, action: {type: string, jwt: string}) => { switch (action.type) { case 'SET_JWT': return {jwt: action.jwt} default: return state } } |
コメントを残す
コメントを投稿するにはログインしてください。