UserContextを使用し、UserReducerを使用してログインを実施


useContextとuseReducerを使用してユーザーログインを検証する

#作業前フォルダ構造


*AppRoute-ユーザーのアクセスを許可するコンポーネントのみ(ログインしていない場合はリダイレクト)


* action.js-論理を構成するフックの位置を初期化するために必要な様々なコンテキストオブジェクト


* reducer.js-初期状態または所望状態を管理する減速機


# 1. プロジェクトの作成とインストールに必要なライブラリ

npx create-react-app login-auth
yarn add react-router-dom
yarn add axios

# 2. フォルダを作成し、各ページコンポーネントを作成します(上図のフォルダルートディレクトリを参照)。


1-1. login.js

// pages/login/index.js

import React from 'react';
import styles from './login.module.css';
 
function Login(props) {
  return (
    <div className={styles.container}>
      <div className={styles.formContainer}>
        <h1>Login Page</h1>
 
        <form>
          <div className={styles.loginForm}>
            <div className={styles.loginFormItem}>
              <label htmlFor='email'>Username</label>
              <input type='text' id='email' />
            </div>
            <div className={styles.loginFormItem}>
              <label htmlFor='password'>Password</label>
              <input type='password' id='password' />
            </div>
          </div>
          <button>login</button>
        </form>
      </div>
    </div>
  );
}
 
export default Login;

1-2. login.css


// pages/login/login.module.css
 
.container {
  min-height: 100vh;
  width: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}
.formContainer {
  width: 200px;
}
.error {
  font-size: 0.8rem;
  color: #bb0000;
}
 
.loginForm {
  display: flex;
  flex-direction: column;
}
 
.loginFormItem {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}

2-1. dashboard.js

// pages/dashboard/index.js
import React from 'react'
import styles from './Dashboard.module.css'

function Dashboard(props) {
   
    return (
        <div style={{ padding: 10 }}>
            <div className={styles.dashboardPage} >
                <h1>
                    Dashboard
                </h1>
                <button className={styles.logoutBtn} >Logout</button>
            </div>
            <p>Welcome to the dashboard</p>
        </div>
    )
}

export default Dashboard

2-2. dashboard.css

// pages/dashboard/dashboard.module.css 
.logoutBtn {
  height: '30px';
  width: '100px';
}

.dashboardPage {
  display: flex;
  width: 100%;
  justify-content: space-between;
}

3-1. dashboard.js

// pages/notFound/index.js
import React from 'react';
import styles from './notfound.module.css';

function NotFound(props) {
	return (
		<div className={styles.container}>
			<h1>Page not found</h1>
		</div>
	);
}

export default NotFound;

3-2. dashboard.css

// pages/notFound/notfound.module.css
.container {
 min-height: 100vh;
 width: 100%;
 display: flex;
 justify-content: center;
 align-items: center;
}

# 3. ルーティングの設定

  • map配線
  • を用いる.
    // Config/routes.js
    import React from 'react'
    import Login from '../pages/login/index'
    import DashBoard from '../pages/dashBoard/index'
    import PageNotFound from '../pages/pageNotFound/index'
    
    const routes =[
      {
        path:'/',
        component: Login
      },
      {
        path:'/dashboard',
        component: Dashboard
      },
      {
        path:'/*',
        component: PageNotFound
      },
    ]
     
    export default routes
  • App.jsファイルにルータ
  • を構成する
    // App.js
    import React from 'react';
    import {
      BrowserRouter as Router,
      Redirect,
      Route,
      Switch,
    } from 'react-router-dom';
    import routes from './Config/routes.js';
     
    function App() {
      return (
        <Router>
          <Switch>
            {routes.map((route) => (
              <Route
                exact
                key={route.path}
                path={route.path}
                component={route.component}
              />
            ))}
          </Switch>
        </Router>
      );
    }
     
    export default App;

    # 4. 検証コンテキストの設定

  • AutheStateContext:このコンテキストには、認証トークンとユーザーの詳細が含まれます.
  • AutheDispatchContext:このコンテキストでは、ユーザモニタ管理ステータスを使用して後で生成されるdispatchが渡されます.これにより、必要なコンポーネントにdispatchを簡単に提供できます.
  • コンテキストオブジェクトのコンテキストを作成します.jsファイルに次の内容を追加します.
    // Context/context.js
     
    import React,{useContext,createContext,useReducer} from "react";
    import {AuthReducer,initialState} from './reducer'
    
    const AuthStateContext = createContext(null)
    const AuthDispatchContext = createContext(null)
    useAuthDispatchuseAuthDispatchフックおよびproviderは、同じファイル(context.js)で生成される.
    
    // Context/context.js
     
    [...]
     
    export function useAuthState() {
      const context = React.useContext(AuthStateContext);
      if (context === undefined) {
        throw new Error("useAuthState는 AuthProvider 안에서만 사용 가능합니다.")
      }
     
      return context;
    }
     
    export function useAuthDispatch() {
      const context = React.useContext(AuthDispatchContext);
      if (context === undefined) {
            throw new Error("useAuthDispatch는 AuthProvider 안에서만 사용 가능합니다.")
      }
     
      return context;
    }
    
    export const AuthProvider =({children})=>{
        const [user,dispatch] = useReducer(AuthReducer,initialState)
    
        return(
            <AuthStateContext.Provider value={user}>
                <AuthDispatchContext.Provider value={dispatch}>
                    {children}
                </AuthDispatchContext.Provider>
            </AuthStateContext.Provider>
        )
    }
    
    このようにして減速機を作っていないので、エラーが発生します.reduce,actionを作成する

    1-1. reducer.js

    
    // Context/reducer.js
    
    let user = localStorage.getItem('currentUser')? JSON.parse(localStorage.getItem('currentUser')).user : '';
    let token = localStorage.getItem('currentUser')? JSON.parse(localStorage.getItem('currentUser')).auth_token : '';
    
    export const initialState ={
        user:""||user,
        token:""||token,
        loading:false,
        errorMessage:null
    }
    
    
    
    export const AuthReducer =(initialState,action)=>{
        switch (action.type){
            case 'REQUEST_LOGIN':
                return{
                    ...initialState,
                    loading: true
                }
            case 'LOGIN_SUCCESS':
                return{
                    ...initialState,
                    user:action.payload.user,
                    token:action.payload.auth_token,
                    loading: false
                }
            case 'LOGOUT':
                return{
                    ...initialState,
                    user:'',
                    token:''
                }
            case 'LOGIN_ERROR':
                return{
                    ...initialState,
                    loading: false,
                    errorMessage: action.error
                }
            default:
                throw new Error( `Unhandled action type: ${action.type}`)
        }
    }
    

    1-2. action.js

    
    // Context/action.js
    
    import axios from "axios";
    const ROOT_URL = 'https://secret-hamlet-03431.herokuapp.com';
    
    
    export const loginUser=async (dispatch,loginPayload)=>{
        const requestOptions={
            url:`${ROOT_URL}/login`,
            method: 'POST',
            headers: {
                'Content-Type':'application/json'
            },
            data:loginPayload
        }
        try{
            const response = await axios(requestOptions)
            if(response.status ===200){
                dispatch({type:'LOGIN_SUCCESS',payload:response.data})
                localStorage.setItem('currentUser',JSON.stringify(response.data))
                return response.data
            }else{
                dispatch({type:'LOGIN_ERROR',error:response.data.error[0]})
            }
            return ;
        }catch (e){
            dispatch({type:'LOGIN_ERROR',error:e})
        }
    }
    
    
    export async function logout(dispatch) {
        dispatch({ type: 'LOGOUT' });
        localStorage.removeItem('currentUser');
        localStorage.removeItem('token');
    }
    
    これで、UserReducerを使用してコンテキストとステータス管理のすべての設定が完了し、Contextフォルダのすべてのコンテンツをエクスポートするフォルダにインデックスが作成されます.jsファイルを生成します.
    import {loginUser,logout,axiosLoginUser} from './actions'
    import {AuthProvider,useAuthState,useAuthDispath} from './context'
    
    export {loginUser,logout,useAuthDispath,useAuthState,AuthProvider,axiosLoginUser}

    # 5. コンテキストの統合

    
    // App.js
    
    import routes from './Config/routes.js';
    import { AuthProvider } from "./Context";
    
    function App() {
      return (
        <AuthProvider>
          <Router>
            <Switch>
              {routes.map((route) => (
                <Route
        		  exact
                  key={route.path}
                  path={route.path}
                  component={route.component}
                />
              ))}
            </Switch>
          </Router>
        </AuthProvider>
      );
    }
    
    export default App;

    # 6. ログイン・ページ・コンポーネントでの認証の実施


    電子メールとパスワード入力フィールドのステータスと入力ハンドルを定義します.
    
    // pages/login/index.js
     
    [...]
     
    function Login(props) {
     
        const [email, setEmail] = useState('')
        const [password, setPassword] = useState('')
     
       
        return (
            <div className={styles.container}>
                <div className={{ width: 200 }}>
                    <h1>Login Page</h1>
                   
                    <form >
                        <div className={styles.loginForm}>
                            <div className={styles.loginFormItem}>
                                <label htmlFor="email">Username</label>
                                <input type="text" id='email' value={email} onChange={(e) => setEmail(e.target.value)} />
                            </div>
                            <div className={styles.loginFormItem}>
                                <label htmlFor="password">Password</label>
                                <input type="password" id='password' value={password} onChange={(e) => setPassword(e.target.value)}  />
                            </div>
                        </div>
                        <button>login</button>
                    </form>
                </div>
            </div>
        )
    }
    [...]
    このとき、サーバへの送信を処理するために使用できるステータスの電子メールとパスワードフィールドを含む関数を作成できます.handleLogin関数呼び出しを作成し、loginボタン呼び出しをクリックします.
    ログインコンポーネントにhandleLogin関数を作成します.
    // pages/login/index.js
     
    [...]
    import {useAuthState,useAuthDispath,axiosLoginUser} from '../../context'
    [...]
    
    function Login(props) {
        const dispatch = useAuthDispath()
        const [email,setEmail] =useState('')
        const [password,setPassword] =useState('')
        const {loading} = useAuthState() //얘는 initialState안에 있는 얘들 구조분해 할당
    
    
        const handleLogin =async (e)=>{
            e.preventDefault()
            let payload = {email,password}
            try{
                const response = await axiosLoginUser(dispatch,payload)
                if(!response.user) return
                props.history.push('/dashboard')
            }catch (e){
                console.error(e)
            }
        }
    
        return (
            <div className={styles.container}>
                <div className={styles.formContainer}>
                    <h1>Login Page</h1>
    
                    <form >
                        <div className={styles.loginForm}>
                            <div className={styles.loginFormItem}>
                                <label htmlFor="email">UserName</label>
                                <input type="text" id={'email'} value={email} onChange={(e)=>setEmail(e.target.value)} disabled={loading}/>
                            </div>
                            <div className={styles.loginFormItem}>
                                <label htmlFor="password">Password</label>
                                <input type="password" id={'password'} value={password} onChange={(e)=>setPassword(e.target.value)} disabled={loading}/>
                            </div>
                        </div>
                        <button onClick={handleLogin}>login</button>
                    </form>
                </div>
            </div>
        );
    }
    
    export default Login;
    登録テストemail: [email protected]
    登録テストpassword: admin123

    #7.ダッシュボードでのログアウトの実施

    // pages/dashboard/index.js
     
    import React from 'react'
    import styles from './dashBoard.module.css'
    import {useAuthState,logout,useAuthDispath} from "../../context";
    
    function Dashboard(props) {
        const dispatch = useAuthDispath()
        const userDetails = useAuthState() // 얘는  initialState
        
    
        const handleLogout=()=>{
            logout(dispatch)
            props.history.push('/login')
        }
    
        return (
            <div style={{ padding: 10 }}>
                <div className={styles.dashboardPage} >
                    <h1>
                        Dashboard
                    </h1>
                    <button className={styles.logoutBtn} onClick={handleLogout}>Logout</button>
                </div>
                <p>Welcome {userDetails.user.email}</p>
            </div>
        )
    }
    
    export default Dashboard
    これにより、ログインとログアウトを簡単に作成できますが、ユーザーが認証を受けていない場合でも、ダッシュボードパスと同じパスにアクセスし続ける可能性があります.

    #8.パス保護の検証


    この問題を解決するには、個人パス(認証されたユーザーのみがアクセスできるパス)を定義し、親コンポーネントを作成して、ユーザーが認証されたときに適切なコンポーネントをレンダリングする必要があります.それ以外の場合は、ログインページにリダイレクトされます.
    まず、パス構成のisPrivateパスがプライベートパスであるかどうかを指定するプロパティを追加します.
    私たちのアプリケーションの2つのパスは、ダッシュボードページと404ページです.
    
    // Config/routes.js
     
    []
    const routes = [
      {
        path: '/login',
        component: Login,
        isPrivate: false,
      },
      {
        path: '/dashboard',
        component: Dashboard,
        isPrivate: true,
      },
      {
        path: '/*',
        component: NotFound,
        isPrivate: true,
      },
    ];
     
     
    export default routes;
    
    次に、パスの保護に役立つコンポーネントを作成します.componentsフォルダを作成し、AppRoutes.jsファイルを作成します.
    Approuteコンポーネントに次の内容を追加できます.
    
    // components/AppRoute.js
     
    import React from 'react';
    import {Redirect,Route} from 'react-router-dom'
    import { useAuthState} from "../context";
    
    function AppRoute({component:Component,path,isPrivate,...rest}) {
        const userDetails = useAuthState()
    
        return (
            <Route
                exact
                path={path}
                render={props =>
                    isPrivate && !Boolean(userDetails.token) ? (
                        <Redirect to={{pathname: '/'}}/>
                    ) : (
                        <Component {...props}/>
                    )
                }
                {...rest}
            />
        );
    }
    
    export default AppRoute;
    

    #8. App.jsの変更

    
    // App.js
    
    
    function App() {
        return (
            <AuthProvider>
                <Router>
                    <Switch>
                        {routes.map((route) => (
                            <AppRoute
                                exact
                                key={route.path}
                                path={route.path}
                                component={route.component}
                                isPrivate={route.isPrivate}
                            />
                        ))}
                    </Switch>
                </Router>
            </AuthProvider>
        );
    }
    
    
    export default AppRoute;
    
    そして今実行してテストすればいいです.