こんにちは、KOUKIです。
ReactでAmbassador画面を実装中です。
今回は、Admin画面の時にも作成した以下の機能を実装します。
- Reduxの導入
- ルーティング設定
- ログイン処理
- ユーザー登録処理
尚、「React, NextJS and Golang: A Rapid Guide – Advanced」コースを参考にしています。解釈は私が勝手に付けているので、本物をみたい場合は受講をお勧めします!
<目次>
前回
前回は、Ambassador画面の環境構築を行いました。
事前準備
フォルダ/ファイル
1 2 3 4 5 6 7 8 9 10 11 12 13 |
mkdir react-ambassador/src/models/ touch react-ambassador/src/models/link.ts touch react-ambassador/src/models/product.ts touch react-ambassador/src/models/user.ts mkdir react-ambassador/src/redux/ mkdir react-ambassador/src/redux/actions/ touch react-ambassador/src/redux/actions/setUserAction.ts mkdir react-ambassador/src/redux/reducers/ touch react-ambassador/src/redux/reducers/setUserReducer.ts mkdir react-ambassador/src/redux/configureStore.ts touch react-ambassador/src/pages/Login.tsx touch react-ambassador/src/pages/Register.tsx touch react-ambassador/src/Login.css |
モジュール
1 2 3 4 5 |
cd react-ambassador/ 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 |
{ "name": "react-ambassador", "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": "^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.8", "@types/redux": "^3.6.0" } } |
モデルの作成
Admin画面と同じように、モデルを用意しましょう。
Linkモデル
1 2 3 4 5 6 |
// models/link.ts export interface Link { id: string code: string } |
Userモデル
1 2 3 4 5 6 7 8 9 10 |
// models/user.ts export class User { id?: number first_name!: string last_name!: string email!: string password?: string password_confirm?: string } |
Productモデル
1 2 3 4 5 6 7 8 9 |
// models/product.ts export interface Product { id: number title: string description: string image: string price: string } |
Reduxの導入
Admin画面で実装したReduxも導入しましょう。
setUserAction.ts
1 2 3 4 5 6 7 8 |
// redux/actions/setUserAction.ts import { 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 |
// redux/reducers/setUserReducer.ts import { User } from "../../models/user" const initialState = { user: new User() } export const setUserReducer = ( state = initialState, action: {type: string, user: User}) => { 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) |
index.tsx
index.tsxに以下の設定をします。
- 共通のURLの定義
- 認証許可
- Reduxの設定
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 |
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' // 共通のURL axios.defaults.baseURL = 'http://localhost:8000/api/ambassador' // Cookieを用いた認証を許可 axios.defaults.withCredentials = true // Reduxの設定 const store = configureStore() ReactDOM.render( <React.StrictMode> <Provider store={store}> <App /> </Provider> </React.StrictMode>, document.getElementById('root') ); reportWebVitals(); |
Layout.ts
Layout.tsに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 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 |
// components/Layout.tsx import { Dispatch, useEffect, useState } from 'react' import { Redirect } from 'react-router-dom' import { connect } from 'react-redux' import { User } from '../models/user' import { setUserAction } from '../redux/actions/setUserAction' import Nav from './Nav' import Header from './Header' import axios from 'axios' const Layout = (props: any) => { const userURL = 'user' const [redirect, setRedirect] = useState(false) 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 /> <main> <Header /> <div className="album py-5 bg-light"> <div className="container"> {props.children} </div> </div> </main> </div> ) } // State const mapStateToProps = (state: {user: User}) => ({ user: state.user }) // Dispatch const mapDispatchToProps = (dispatch: Dispatch<any>) => ({ // setUser(Action)->dispatch setUser: (user: User) => dispatch(setUserAction(user)) }) // LayoutコンポーネントをRedux Storeに登録 export default connect(mapStateToProps, mapDispatchToProps)(Layout) |
Pages
Admin画面と同じように、ログイン画面と登録画面を作成しましょう。
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 |
// pages/Login.tsx import { SyntheticEvent, useState } from 'react' import '../Login.css' import axios from 'axios' import { Redirect } from 'react-router' const Login = () => { // ログイン情報 const [email, setEmail] = useState('') const [password, setPassword] = useState('') const loginUrl = 'login' const [redirect, setRedirect] = useState(false) const submit = async (e: SyntheticEvent) => { // formのデフォルトの挙動をキャンセル e.preventDefault() // ログイン情報を送信 await axios.post(loginUrl, { email: email, password: password // withCredentials // リクエストに Cookie を添えて送信する // API側ではCookieにTokenを保存している }) // リダイレクトフラグをTrue setRedirect(true) } if (redirect) { // Homeへリダイレクトする return <Redirect to={'/'} /> } return ( <main className="form-signin"> <form onSubmit={submit}> <h1 className="h3 mb-3 fw-normal">Please sign in</h1> <div className="form-floating"> <input type="email" className="form-control" id="floatingInput" placeholder="name@example.com" // Emailをセット onChange={e => setEmail(e.target.value)} /> <label htmlFor="floatingInput">Email address</label> </div> <div className="form-floating"> <input type="password" className="form-control" id="floatingPassword" placeholder="Password" // Passwordをセット onChange={e => setPassword(e.target.value)} /> <label htmlFor="floatingPassword">Password</label> </div> <button className="w-100 btn btn-lg btn-primary" type="submit">Sign in</button> </form> </main> ) } export default Login |
Login.css
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/Login.css */ .form-signin { width: 100%; max-width: 330px; padding: 15px; margin: auto; } .form-signin .checkbox { font-weight: 400; } .form-signin .form-floating:focus-within { z-index: 2; } .form-signin input[type="email"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } |
Register.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 |
// pages/Register.tsx import {Component, SyntheticEvent} from "react"; import axios from "axios" import {User} from '../models/user' import {Redirect} from 'react-router-dom' class Register extends Component { firstName = '' lastName = '' email = '' password = '' passwordConfirm = '' registerUrl = 'register' state = { redirect: false } // SyntheticEvent // https://ja.reactjs.org/docs/events.html submit = async (e: SyntheticEvent) => { // formのデフォルトの挙動をキャンセルする // https://ja.reactjs.org/docs/handling-events.html e.preventDefault() const user = new User( this.firstName, this.lastName, this.email, this.password, this.passwordConfirm ) // フォーマットが合っていればクラスをそのまま渡せる await axios.post(this.registerUrl, user) // 送信に成功したらリダイレクトフラグを立てる this.setState({ redirect: true }) } render() { // ログイン画面へリダイレクト if (this.state.redirect) { return <Redirect to={'/login'} /> } return ( <main className="form-signin"> <form onSubmit={this.submit}> <h1 className="h3 mb-3 fw-normal">Please register</h1> <div className="form-floating"> <input className="form-control" placeholder="First Name" onChange={e => this.firstName = e.target.value} /> <label>First Name</label> </div> <div className="form-floating"> <input className="form-control" placeholder="Last Name" onChange={e => this.lastName = e.target.value} /> <label>Last Name</label> </div> <div className="form-floating"> <input type="email" className="form-control" placeholder="name@example.com" onChange={e => this.email = e.target.value} /> <label>Email address</label> </div> <div className="form-floating"> <input type="password" className="form-control" placeholder="Password" onChange={e => this.password = e.target.value} /> <label>Password</label> </div> <div className="form-floating"> <input type="password" className="form-control" placeholder="Password Confirm" onChange={e => this.passwordConfirm = e.target.value} /> <label>Password Confirm</label> </div> <button className="w-100 btn btn-lg btn-primary" type="submit">Submit</button> </form> </main> ) } } export default Register |
App.css
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 |
/* App.css */ body { font-size: 0.875rem; } .feather { width: 16px; height: 16px; vertical-align: text-bottom; } /* * Sidebar */ .sidebar { position: fixed; top: 0; /* rtl:raw: right: 0; */ bottom: 0; /* rtl:remove */ left: 0; z-index: 100; /* Behind the navbar */ padding: 48px 0 0; /* Height of navbar */ box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.1); } @media (max-width: 767.98px) { .sidebar { top: 5rem; } } .sidebar-sticky { position: relative; top: 0; height: calc(100vh - 48px); padding-top: 0.5rem; overflow-x: hidden; overflow-y: auto; /* Scrollable contents if viewport is shorter than content. */ } .sidebar .nav-link { font-weight: 500; color: #333; } .sidebar .nav-link .feather { margin-right: 4px; color: #727272; } .sidebar .nav-link.active { color: #2470dc; } .sidebar .nav-link:hover .feather, .sidebar .nav-link.active .feather { color: inherit; } .sidebar-heading { font-size: 0.75rem; text-transform: uppercase; } /* * Navbar */ .navbar-brand { padding-top: 0.75rem; padding-bottom: 0.75rem; font-size: 1rem; background-color: rgba(0, 0, 0, 0.25); box-shadow: inset -1px 0 0 rgba(0, 0, 0, 0.25); } .navbar .navbar-toggler { top: 0.25rem; right: 1rem; } .navbar .form-control { padding: 0.75rem 1rem; border-width: 0; border-radius: 0; } .form-control-dark { color: #fff; background-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.1); } .form-control-dark:focus { border-color: transparent; box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.25); } |
index.css
1 |
/* 後で実装するので空 */ |
ルートの設定
ログイン画面と登録画面へのルートを設定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
// App.tsx import { BrowserRouter, Route } from 'react-router-dom' import ProductsFrontend from './pages/ProductsFrontend' import Login from './pages/Login' import Register from './pages/Register' import './App.css'; function App() { return ( <BrowserRouter> <Route path={'/'} exact component={ProductsFrontend} /> <Route path={'/login'} component={Login} /> <Route path={'/register'} component={Register} /> </BrowserRouter> ); } export default App; |
ログイン画面
ブラウザから「http://localhost:4000/login」へアクセスします。

登録画面
ブラウザから「http://localhost:4000/register」へアクセスします。

デモ
次回
次回は、ナビゲーションを実装しましょう。
コメントを残す
コメントを投稿するにはログインしてください。