+2

Login trong Reactjs sử dụng axios với redux

Giới thiệu

Cài đặt

Ở đây mình sẽ sử dụng them react-router-dom, material-ui và react-hook-form

npx create-react-app learn-reactjs
npm install --save react-hook-form
npm install --save react-router-dom
npm install @reduxjs/toolkit react-redux
npm install --save axios
cd learn-reactjs
npm start

Tạo các thư mục

learn-reactjs
├─ build
├─ node_modules
├─ public
└─ src
  ├─ api
  │  ├─ axiosClient.js 
  │  └─ userApi.js
  ├─ constants
  │  └─ storage-keys.js
  ├─ app
  │  └─ store.js
  ├─ components
  │  └─ form-control
  │     ├─  InputField
  │     │  └─ index.jsx
  │     └─ PasswordField
  │        └─ index.jsx
  └─ features 
  │  └─ Auth
  │     ├─ userSlice.js
  │     ├─ components
  │     │  ├─ Login.jsx 
  │     │  └─ LoginForm.jsx 
  │     ├─ page
  │     │  └─ LoginPage.jsx 
  │     └─ index.jsx
  ├─ App.css
  ├─ App.js
  └─ index.js

App

Chỉnh sửa file index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter } from 'react-router-dom';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';
import { Provider } from 'react-redux';
import store from './app/store';

ReactDOM.render(
  <React.StrictMode>
      <Provider store={store}>
          <BrowserRouter>
              <App />
          </BrowserRouter>
      </Provider>
  </React.StrictMode>,
  document.getElementById('root')
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Chỉnh sửa file app.js

import { Redirect, Route, Switch } from 'react-router-dom';
import './App.css';
import CounterFeature from './features/Counter';

function App() {

  return (
    <div className="app">
      <Switch>
        <Redirect from="home" to="/" exact />
       <Route path="/login" component={LoginFeature}/>
      </Switch>
    </div>
  );
}

export default App;

Form Control

InputField

Chỉnh sửa file index.jsx trong thư mục InputField

import { TextField } from '@material-ui/core';
import PropTypes from 'prop-types';
import React from 'react';
import { Controller } from "react-hook-form";
import { FormHelperText } from '@material-ui/core';



InputField.propTypes = {
    form: PropTypes.object.isRequired,
    name: PropTypes.string.isRequired,
    label: PropTypes.string,
    disabled: PropTypes.bool,
};

function InputField(props) {

    const { form, name, label, disabled } = props
    const { formState: { errors } } = form
    const hasError = errors[name]

    return (
        <div>
            <Controller
                control={form.control}
                name={name}
                render={({ field }) => (
                    <TextField
                        {...field}
                        fullWidth
                        margin="normal"
                        variant="outlined"
                        label={label}
                        disabled={disabled}
                        error={!!hasError}
                    />
                )}
            />
            <FormHelperText error={!!hasError}>
                {errors[name]?.message}
            </FormHelperText>
        </div>
    );
}

export default InputField;

PasswordField

Chỉnh sửa file index.jsx trong thư mục PasswordField

import { FormHelperText } from '@material-ui/core';
import FormControl from '@material-ui/core/FormControl';
import IconButton from '@material-ui/core/IconButton';
import InputAdornment from '@material-ui/core/InputAdornment';
import InputLabel from '@material-ui/core/InputLabel';
import OutlinedInput from '@material-ui/core/OutlinedInput';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
import React, { useState } from 'react';
import { Controller } from "react-hook-form";

PasswordField.propTypes = {

};

function PasswordField(props) {

    const { form, name, label, disabled } = props
    const { formState: { errors } } = form
    const hasError = errors[name]
    const [showPassword, setShowPassword] = useState(false)

    const toggleShowPassword = () => {
        setShowPassword(!showPassword)
    }

    return (
        <div>
            <FormControl  error={!!hasError} variant="outlined" margin="normal" fullWidth>
                <InputLabel htmlFor={name}>{label}</InputLabel>
                <Controller
                    control={form.control}
                    name={name}
                    render={({ field }) => (
                        <OutlinedInput
                            {...field}
                            id={name}
                            type={showPassword ? 'text' : 'password'}
                            label={label}
                            endAdornment={
                                <InputAdornment position="end">
                                    <IconButton
                                        aria-label="toggle password visibility"
                                        onClick={toggleShowPassword}
                                        edge="end"
                                    >
                                        {showPassword ? <Visibility /> : <VisibilityOff />}
                                    </IconButton>
                                </InputAdornment>
                            }
                            disabled={disabled}
                            error={!!hasError}
                            helperText={errors[name]?.message}
                            labelWidth={70}
                        />
                    )}
                />
                <FormHelperText>
                    {errors[name]?.message}
                </FormHelperText>
            </FormControl>
        </div>
    );
}

export default PasswordField;

Constants

Chỉnh sửa file storage-keys.js

const StorageKeys = {
    user: 'user',
    access: 'access_token',
    refresh: 'refresh_token',
}
export default StorageKeys

Api

axiosClient

Bạn tạo một phiên bản axios mới với cấu hình tùy chỉnh bằng cách chỉnh sửa file axiosClient.js trong thư mục api.

import axios from 'axios';

const axiosClient = axios.create({
    baseURL: 'http://127.0.0.1:8000/',
    headers: {
        'content-type': 'application/json',
    }
})

userApi


import StorageKeys from "../constants/storage-keys";
import axiosClient from "./axiosClient";

const userApi = {
    register(data) {
        const url = 'register/';
        return axiosClient.post(url, data);
    },
    login(data) {
        const url = '/api/token/';
        return axiosClient.post(url, data);
    },
    async getUser(params) {
        const newParams = { ...params }
        const accessToken = localStorage.getItem(StorageKeys.access)
        const url = `users/`;
        const response = await axiosClient.get(url, {
            params: { ...newParams },
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        });
        return response
    },
    async getProfile(params) {
        const newParams = { ...params }
        const accessToken = localStorage.getItem(StorageKeys.access)
        const response = await axiosClient.get(`/detail/`, {
            params: { ...newParams },
            headers: {
                Authorization: `Bearer ${accessToken}`
            }
        })
        return response
    },
}

export default userApi

Auth

Tạo một slice user state cho Redux

Chỉnh sửa file userSlice.js trong thư mục Auth

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import userApi from '../../api/userApi';
import StorageKeys from '../../constants/storage-keys';

// createAsyncThunk cái này sử dụng cho login và register
export const register = createAsyncThunk(
    'users/register',
    async (payload) => {
        //call api to register
        return data;
    }
)

// createAsyncThunk cái này sử dụng cho login và register
export const login = createAsyncThunk(
    'users/login',
    async (payload) => {
        try {
            const response = await authApi.login(payload);
            localStorage.setItem(StorageKeys.access, response.data.access);
            localStorage.setItem(StorageKeys.refresh, response.data.refresh);
            const username = JSON.parse(response.config.data).username
            const responseUser = await authApi.getUser({ username: username })
            const user = {...responseUser.data[0]}
            const responseProfile = await authApi.getProfile({user: user.id})
            const profile = {...responseProfile.data}
            const data = {
                ...user,
                ...profile,
            }
            localStorage.setItem(StorageKeys.user, JSON.stringify(data));
            return data
        } catch (error) {
            console.log(error)
            return error.message;
        }
    }
)

const userSlice = createSlice({
    name: 'user',
    initialState: {
        current: JSON.parse(localStorage.getItem(StorageKeys.USER)) || {},
        settings: {},
    },
    reducers: {
        logout(state) {
            //clear local storage
            state.current = {}
            localStorage.removeItem(StorageKeys.access)
            localStorage.removeItem(StorageKeys.refresh)
            localStorage.removeItem(StorageKeys.user)
        }
    },
    extraReducers: {
        [register.fulfilled]: (state, action) => {
            state.current = action.payload;
        },

        [login.fulfilled]: (state, action) => {
            state.current = action.payload;
        }
    }
})

const { actions, reducer } = userSlice
export const {logout} = actions
export default reducer

Store

Tạo một Redux Store

Tạo một Redux Store bằng cách chỉnh sửa file store.js trong thư mục store

import userReducer from '../features/Auth/userSlice'

const { configureStore } = require("@reduxjs/toolkit");


const rootReducer = {
    user: userReducer,
}

const store = configureStore({
    reducer: rootReducer,
})

export default store

Login

Chỉnh sửa file index.jsx trong thư mục Auth

import React from 'react';
import { Route, Switch, useRouteMatch } from 'react-router-dom';
import LoginPage from './page/LoginPage';
import { Box } from '@material-ui/core';


LoginFeature.propTypes = {
    
};

function LoginFeature(props) {
    const match = useRouteMatch()

    return (
        <div>
            <Box pt={4}>
                <Switch>
                    <Route path={match.url} component={LoginPage} exact />
                </Switch>
            </Box>
        </div>
    );
}

export default LoginFeature;

Chỉnh sửa file LoginPage.jsx trong thư mục Auth/page

import { makeStyles } from '@material-ui/core';
import React from 'react';
import { useSelector } from 'react-redux';
import LoginForm from '../components/LoginForm';



const useStyles = makeStyles((theme) => ({
    root: {
        padding: theme.spacing(4, 50),
    },
}))

LoginPage.propTypes = {

};

function LoginPage(props) {
    const classes = useStyles();
    const loginInUser = useSelector(state => state.user.current)
    const isLoggedIn = !!loginInUser.id

    return (
        <div className={classes.root}>
            {!isLoggedIn && (
                <>
                    <LoginForm />
                </>
            )}
            {isLoggedIn && (
                <div>
                    <h2>Is Login</h2>
                </div>
            )}
        </div>
    );
}

export default LoginPage;

Chỉnh sửa file Login.jsx

import { unwrapResult } from '@reduxjs/toolkit';
import PropTypes from 'prop-types';
import React from 'react';
import { useDispatch } from 'react-redux';
import { login } from '../../userSlice';
import LoginForm from '../LoginForm';


Login.propTypes = {
};

function Login(props) {
    const dispatch = useDispatch()

    const handleSubmit = async (values) => {
        try {
            const actions = login(values)
            await dispatch(actions)
        } catch (error) {
            console.log(error)
        }
    }

    return (
        <div>
            <LoginForm onSubmit={handleSubmit} />
        </div>
    );
}

export default Login;

Chỉnh sửa file LoginForm.jsx

import { yupResolver } from '@hookform/resolvers/yup';
import { Avatar, Button, LinearProgress, makeStyles, Typography } from '@material-ui/core';
import LockOutlined from '@material-ui/icons/LockOutlined';
import PropTypes from 'prop-types';
import React from 'react';
import { useForm } from 'react-hook-form';
import * as yup from "yup";
import InputField from '../../../../components/form-control/InputField';
import PasswordField from '../../../../components/form-control/PasswordField';

LoginForm.propTypes = {
    onSubmit: PropTypes.func,
};

const useStyles = makeStyles((theme) => ({
    root: {
        padding: theme.spacing(2, 2),
    },
    avatar: {
        margin: "0 auto 15px",
        backgroundColor: theme.palette.secondary.main,
    },
    title: {
        textAlign: "center",
    },
    submit: {
        margin: theme.spacing(2, 0, 0, 0),
    },
    linearProgress: {
        margin: theme.spacing(0, 0, 4, 0)
    }
}))

function LoginForm(props) {

    const classes = useStyles();
    const { onSubmit } = props

    const schema = yup.object().shape({
        identifier: yup.string()
            .required("Please enter your email.")
            .email("Please enter a valid email"),
        password: yup.string()
            .required("Please enter your password.")      
    });

    const form = useForm({
        defaultValues: {
            identifier: '',
            password: '',
        },
        resolver: yupResolver(schema),
    })


    const handleSubmit = async (values) => {
        if (onSubmit) {
            await onSubmit(values)
        }
    }

    const { isSubmitting } = form.formState
    
    return (
        <div className={classes.root}>
            {isSubmitting && <LinearProgress className={classes.linearProgress} color="secondary" />}
            <Avatar className={classes.avatar}>
                <LockOutlined />
            </Avatar>
            <Typography component="h3" variant="h5" className={classes.title}>
                Sign In
            </Typography>
            <form onSubmit={form.handleSubmit(handleSubmit)}>
                <InputField name="identifier" label="Email" form={form} />
                <PasswordField name="password" label="Password" form={form} />
                <Button disabled={isSubmitting} type="submit" variant="contained" color="primary" fullWidth className={classes.submit}>
                    Sign in
                </Button>
            </form>
        </div>
    );
}

export default LoginForm;

Bài viết đến đây là kết thúc. Chúc các bạn thành công


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí