# React Router solution ## Add React Router At this time, you've already have the design for the Job Routing app. Let's say: - Home page lives inside the `<Homepage/>` component - The pop-up modal (aka single detail page of the job) is inside the `<JobPage/>`. Here is the router layout for those two pages together with the `<NotFoundPage/>` in `App.js` ```jsx= import { Route, Routes } from 'react-router-dom import HomePage from './pages/HomePage'; import Layout from './layout/Layout'; import NotFoundPage from './pages/NotFoundPage'; export default function App(){ return ( <Routes> <Route path="/" element={<Layout />}> <Route index element={<HomePage />} /> <Route path="*" element={<NotFoundPage />} /> </Route> </Routes> ) } ``` in `./layout/Layout` ```jsx= import { Link, Outlet } from "react-router-dom"; import NavBar from "../components/Navbar"; const style = { container: { maxWidth: "1140px", margin: "auto" } }; const Layout = () => { return ( <> <NavBar /> <div style={style.container}> <Outlet /> </div> </> ); }; export default Layout; ``` ## Render jobs with json-server ### Jobs data Download jobs data from [this](https://drive.google.com/file/d/175JD05bo6Cjn3XupqlBf83n2Jta1Ymz1/view?usp=sharing) file Put the `jobs.json` in your project Run this command to trigger your json-server `npx json-server --watch -p 5000 jobs.json` in `.env` ```jsx= REACT_APP_BASE_URL=http://localhost:5000 ``` Read more about how to use [json-server](https://www.npmjs.com/package/json-server) ### Create Job Context 1. Set up Job Context We are now will combine two hooks `useContext` and `useReducer` to handle the Job Context. A short note for their usage: - `useContext`: is used to create common data that can be accessed throughout the component hierarchy without passing the props down manually to each level - `useReducer`: is usually preferable to useState when you have complex state logic that involves multiple sub-values or when the next state depends on the previous one. In the code snippet below, we will note as if the line of code is using for either useContext or useReducer in `./contexts/JobContext.js` ```jsx= import { createContext, useReducer } from 'react'; //Step 1: Initial State and Actions (useReducer) const initialState = { jobs: [], total: 0, page: 1, limit: 5, q: '', error: null, loading: false }; const GET_JOB_LOADING = 'GET_JOB_LOADING'; const GET_JOB_SUCCESS = 'GET_JOB_SUCCESS'; const GET_JOB_FAILURE = 'GET_JOB_FAILURE'; const CHANGE_PAGE = 'CHANGE_PAGE'; //Step 2: Reducer to handle Actions (useReducer) const reducer = (state, action) => { switch (action.type) { case GET_JOB_LOADING: return {...state, loading: true}; case GET_JOB_SUCCESS: return { ...state, loading: false, ...action.payload, }; case GET_JOB_FAILURE: return { ...state, loading: false, jobs: [], error: action.payload, }; default: return state; } }; //Step 3: Create Job Context const JobContext = createContext(); //Step4: Create Job Provider const JobProvider = ({ children }) => { const [state, dispatch] = useReducer(reducer, initialState); //Create a function to fetch jobs data const getJobList = async ({ page = state.page, limit = state.limit, q = state.q }, callback) => { try { //Start fetching data dispatch({ type: GET_JOB_LOADING }); const query = q ? `q=${q}` : '' const url = `${process.env.REACT_APP_BASE_URL}/jobs?_page=${page}&_limit=${limit}&${query}` const res = await fetch(url); const totalItems = res.headers.get('x-total-count'); const data = await res.json(); //Successfully get jobs data from the API dispatch({ type: GET_JOB_SUCCESS, payload: { jobs: data, total: totalItems } }); callback(); } catch (err) { //Fail in fetch jobs data dispatch({ type: GET_JOB_FAILURE, payload: err }); callback(); } }; //JobContext Provider: pass all the value through children return <JobContext.Provider value={{ ...state, getJobList }}>{children}</JobContext.Provider>; }; //Step 5 : Export JobContext and JobProvider export { JobContext, JobProvider }; ``` Wrap all the components that we want to use the value from `JobContext` inside the `JobProvider` in `app.js` ```jsx= --- return ( <JobProvider> <Routes> <Route path="/" element={<Layout />}> <Route index element={<HomePage />} /> <Route path="*" element={<NotFoundPage />} /> </Route> </Routes> </JobProvider> ) ``` 2. use JobContext First, wrap all the component that we want to use the JobContext. in `App.js` ```jsx= import { Route, Routes } from 'react-router-dom import HomePage from './pages/HomePage'; import Layout from './layout/Layout'; import NotFoundPage from './pages/NotFoundPage'; import { JobProvider } from './contexts/JobContext'; export default function App(){ return ( <JobProvider> <Routes> <Route path="/" element={<Layout />}> <Route index element={<HomePage />} /> <Route path="*" element={<NotFoundPage />} /> </Route> </Routes> </JobProvider> ) } ``` Seconds, create a Custom Hook to call out the Job Context. For now, we only have to use useJob hook to retrieve data from `JobContext` in `./hooks/useJob.js` ```jsx= import { useContext } from 'react'; import { JobContext } from '../contexts/JobContext'; const useJob = () => { return useContext(JobContext); }; export default useJob; ``` in `./pages/HomePage` ```jsx= import { useEffect, useState } from 'react'; import JobList from '../components/JobList'; import PaginationBar from '../components/PaginationBar'; import useJob from '../hooks/useJob'; const HomePage = () => { //When it comes to retrieve data from JobContext, use useJob instead of useContext(JobContext) const { getJobList, jobs, loading } = useJob(); useEffect(() => { getJobList(); }, []); return ( <div> {loading ? <h1>Loading</h1> : <JobList jobs={jobs} />} <PaginationBar /> </div> ); }; export default HomePage; ``` in `./components/JobList` ```jsx= import { Box } from '@mui/material'; import JobCard from './JobCard'; const JobList = ({ jobs }) => { return ( <Box sx={{ display: 'flex', flexWrap: 'wrap', m: 6, gap: 6, '& > :not(style)': { width: 300, height: 240, padding: 2, }, }} > {jobs.length && jobs.map((job) => <JobCard key={job.id} job={job} />)} </Box> ); }; export default JobList; ``` Let's break down the flow here to understand what `useContext` do. 1. When we at the url `http://localhost:3000`, React render `<HomePage/>` 2. `useJob` run, then we retrieve - `getJobList` as a function - `jobs` at its initialState which is empty array (`[]`) - `loading` at its initialState (`false`) 3. `jobs.length` is 0 at the moment, so nothing is rendered on the screen in term of component `<JobList/>` 4. After `return`, now the function inside `useEffect` runs 5. `getJobList` is defined in `<JobProvider/>`, take a step backward to `<JobProvider/>`. Break down to steps of what `getJobList` do to see how we can handle fetching logic with `useReducer`: - `dispatch({ type: GET_JOB_LOADING });`: start fetching data - Update `loading` to `true` in our Job Context :arrow_right: `<HomePage/>` re-render because it use `loading` from the Job Context. - `dispatch({ type: GET_JOB_SUCCESS, payload: { jobs: data, total: totalItems } });`: successfully fetching data from API: - `const data = await res.json();`: able to get the data from API without error, the use `data` and `totalItems` as a payload, dispatch it to reducer - Reducer helps to update `jobs`, `total` and `loading` in our Job Context. :arrow_right: `<HomePage/>` re-render because it use `loading` from the Job Context. :arrow_right: `JobList` render the first time and receive `jobs` as props - `dispatch({ type: GET_JOB_FAILURE, payload: err });`: if we fail to fetch data, error will be catch and action with type `GET_JOB_FAILURE` will be dispatch, then: - Update `loading` to `false` in our Job Context :arrow_right: `<HomePage/>` re-render. ### Addition: Navbar ## Login This login feature is sponsored by React Router auth [example](https://github.com/remix-run/react-router/tree/main/examples/auth) Let's split them into steps to deeply understand the flow. <!-- ### Step 2: Fake login function in `./apiServices` ```jsx= const fakeAuth = { isAuthenticated: false, signin: (callback) => { fakeAuth.isAuthenticated = true; setTimeout(callback, 100); }, signout: (callback) => { fakeAuth.isAuthenticated = false; setTimeout(callback, 100); } }; export default fakeAuth; ``` This object helps us fake sign in and out without api needed. `fakeAuth` is an object that contain three key: - `isAuthenticated` is the status of user whether they login or not - `sigin` is the function that take a `callback` function as an argument that update the status `isAuthenticated` to true. Then run the `callback` function after 100 milisecond. - `signout` is the function that take a `callback` function as an argument that update the status `isAuthenticated` to false. Then run the `callback` function after 100 milisecond. We'll see what should be the `callback` function in later step. --> ### Step 1: Create Auth context in `./contexts/AuthContext` ```jsx= import { createContext, useState } from 'react'; export const AuthContext = createContext(null); export const AuthProvider = ({ children }) => { const [user, setUser] = useState(null); const [password, setPassword] = useState(null); let signin = (newUser, newPassword, redirect) => { setUser(newUser); setPassword(newPassword); redirect(); }; let signout = (redirect) => { setUser(null); setPassword(null); redirect(); }; let value = { user, signin, signout }; return ( <AuthContext.Provider value={value}>{children} </AuthContext.Provider> ); }; ``` If you're not familiar with the `{children}` concept, please have a look on this [article](https://codeburst.io/a-complete-guide-to-props-children-in-react-c315fab74e7c) In short, `children` is a key of `props` which is the reason for the destructure syntax `{children}` With the AuthContect set up above, we now can use the object `value` for `children` ### Step 2: Use AuthContext in our components As the steps of using AuthContext is similar to JobContext, we'll go through it quickly with the code snippet below. Wrap around component with JobProvider. in `App.js` ```jsx= import { Route, Routes } from 'react-router-dom import HomePage from './pages/HomePage'; import Layout from './layout/Layout'; import NotFoundPage from './pages/NotFoundPage'; import { JobProvider } from './contexts/JobContext'; import { AuthProvider } from './contexts/AuthProvider'; export default function App(){ return ( <AuthProvider> <JobProvider> <Routes> <Route path="/" element={<Layout />}> <Route index element={<HomePage />} /> <Route path="*" element={<NotFoundPage />} /> </Route> </Routes> </JobProvider> </AuthProvider> ) } ``` Let's create a custom hook for our AuthContext, just like what we did with the JobContext. in `.hooks/useAuth.js` ```jsx= import { useContext } from 'react'; import { AuthContext } from '../contexts/AuthContext'; const useAuth = () => { return useContext(AuthContext); }; export default useAuth; ``` ### Step 4: Handle login data with React Hook Form #### Create the form folder with the the `FormProvider` and `FtextField` `./form/FormProvider.js` ```jsx= import { FormProvider as RHFormProvider } from "react-hook-form"; const FormProvider = ({ children, onSubmit, methods }) => { return ( <RHFormProvider {...methods}> <form onSubmit={onSubmit}>{children}</form> </RHFormProvider> ); }; export default FormProvider; ``` `./form/FTextField.js` ```jsx= import { TextField } from "@mui/material"; import { Controller, useFormContext } from "react-hook-form"; const FTextField = ({ name, ...other }) => { const { control } = useFormContext(); return ( <Controller name={name} control={control} render={({ field, fieldState: { error } }) => ( <TextField {...field} {...other} fullWidth error={!!error} /> )} /> ); }; export default FTextField; ``` `./form/index.js` ```jsx= export { default as FormProvider } from "./FormProvider"; export { default as FTextField } from "./FTextField"; ``` #### Login Modal 1. Fist, let's get a example modal from Material UI. Then implement react hook form with the default value is the object `{username: 'web virgil learner', password: 123456}` ```jsx= import { useState } from 'react'; import Box from '@mui/material/Box'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; import Modal from '@mui/material/Modal'; const style = { marginTop: 8, display: 'flex', flexDirection: 'column', alignItems: 'center', backgroundImage: 'linear-gradient(to bottom, #323232 0%, #3F3F3F 40%, #1C1C1C 150%), linear-gradient(to top, rgba(255,255,255,0.40) 0%, rgba(0,0,0,0.25) 200%)', padding: 5, }; export default function LoginModal(open, setOpen) { // const [open, setOpen] = useState(false); const handleClose = () => setOpen(true); const defaultValues = { username: 'web virgil learner', password: 123456, }; const methods = useForm({ defaultValues }); const { handleSubmit } = methods; const onSubmit = (data) => { console.log(data) }; return ( <div> <Modal open={open} onClose={handleClose} aria-labelledby="modal-modal-title" aria-describedby="modal-modal-description" > <Box sx={style}> <Typography component="h1" variant="h5"> Log in </Typography> <Box sx={{ mt: 1 }}> <FormProvider methods={methods} onSubmit={handleSubmit(onSubmit)}> <FTextField name="username" label="User name" style={{ marginBottom: '1rem' }} /> <FTextField name="password" label="Password" style={{ marginBottom: '1rem' }} /> <Button type="submit" fullWidth variant="contained" sx={{ mt: 1, mb: 2 }}> Sign In </Button> </FormProvider> </Box> </Box> </Modal> </div> ); } ``` ### Step 5: Checking authentication ## Job Modal and Login Modal as a page with React Router