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