Try   HackMD

WEEK 5 - Router and Context

tags: WEB JS DIGITALIZE FRONTEND REACTJS

Component Lifecycle

Each component has several lifecycle methods that you can override to run code at particular times in the process.

Image Not Showing Possible Reasons
  • The image file may be corrupted
  • The server hosting the image is unavailable
  • The image path is incorrect
  • The image format is not supported
Learn More →

  • Mounting
    Mounting means putting elements into the DOM.
    React has four built-in methods that gets called, in this order, when mounting a component:

    1. constructor()
    2. getDerivedStateFromProps()
    3. render()
    4. componentDidMount()

    The render() method is required and will always be called, the others are optional and will be called if you define them.

Example:

import React, { Component } from 'react';

class ShowHide extends Component {
    constructor(props) {
        super(props)// props should be passed to superclass
        this.state = { // init state 
            showMsg: false
        }
        this.showHide = this.showHide.bind(this) // bninding function to this class
    }
    // this executed after constructor and before render functions
    static getDerivedStateFromProps(props, state)
    {
        console.log(props) // old one
        console.log(state) // old one
        return { // return new values of the state
            showMsg:true
        }
    }
    showHide() {
        this.setState({
            showMsg: !this.state.showMsg
        })
    }
    render() {
        console.log('rendering after getDerivedStateFromProps function', this.state)
        let { showMsg } = this.state

        return <div>
            <button onClick={this.showHide}>Show/Hide</button>
            {showMsg && <h1>Hello, there</h1>}
        </div>
    }
}

export default ShowHide;
  • Updating
    The next phase in the lifecycle is when a component is updated.

A component is updated whenever there is a change in the component's state or props.

React has five built-in methods that gets called, in this order, when a component is updated:
1. getDerivedStateFromProps()
2. shouldComponentUpdate()
3. render()
4. getSnapshotBeforeUpdate()
5. componentDidUpdate()

The render() method is required and will always be called, the others are optional and will be called if you define them.

import React, { Component } from 'react';

class ShowHide extends Component {
    constructor(props) {
        super(props)// props should be passed to superclass
        
        this.state = { // init state 
            showMsg: false
        }
        this.showHide = this.showHide.bind(this) // bninding function to this class
        console.log('constructor')
    }
    // this function added to understand how component will not changed on update!
     
    shouldComponentUpdate() {
        return false;
      }
    // this executed after constructor and before render functions
    static getDerivedStateFromProps(props, state)
    {
        console.log('getDerivedStateFromProps')
        console.log(props) // old one
        console.log(state) // old one
        return { // return new values of the state
            showMsg:true
        }
    }
    showHide() {
        this.setState({
            showMsg: !this.state.showMsg
        })
    }
    render() {
        console.log('render')
        console.log('rendering after getDerivedStateFromProps function', this.state)
        let { showMsg } = this.state

        return <div>
            <button onClick={this.showHide}>Show/Hide</button>
            {showMsg && <h1>Hello, there</h1>}
        </div>
    }
    componentDidMount()
    {
        console.log('componentDidMount')
        console.log("Executed after render function and after component amounted to the DOM")
        console.log('Here you can make heavy & side effetcs tasks')
    }
}

export default ShowHide;
  • Unmounting
    The next phase in the lifecycle is when a component is removed from the DOM, or unmounting as React likes to call it.

React has only one built-in method that gets called when a component is unmounted:

  1. componentWillUnmount()

ReactJS Hooks

Hooks are the new feature introduced in the React 16.8 version. It allows you to use state and other React features without writing a class. Hooks are the functions which "hook into" React state and lifecycle features from function components. It does not work inside classes.

We can simply imagine as it the migration from class component to functional and make the latest one to be used as statefull component with additional capabilities and features.

useState()

There is no constructor function and instead of that we can use useState() function inside the function component to create and manage states of the component.
Example:

import React, { useState } from 'react';  
  
function CountApp() {  
  // Declare a new state variable, which we'll call "count"  
  const [count, setCount] = useState(0);  
  
  return (  
    <div>  
      <p>You clicked {count} times</p>  
      <button onClick={() => setCount(count + 1)}>  
        Click me  
      </button>  
    </div>  
  );  
}  
export default CountApp; 

useEffect()

The Effect Hook allows us to perform side effects (an action) in the function components. It does not use components lifecycle methods which are available in class components. In other words, Effects Hooks are equivalent to componentDidMount(), componentDidUpdate(), and componentWillUnmount() lifecycle methods.

Side effects have common features which the most web applications need to perform, such as:

  • Updating the DOM,
  • Fetching and consuming data from a server API,
  • Setting up a subscription, etc.

Example:

import React, { useState, useEffect } from 'react';  
  
function CounterExample() {  
  const [count, setCount] = useState(0);  
  
  // Similar to componentDidMount and componentDidUpdate:  
  useEffect(() => {  
    // Update the document title using the browser API  
    document.title = `You clicked ${count} times`;  
  });  
  
  return (  
    <div>  
      <p>You clicked {count} times</p>  
      <button onClick={() => setCount(count + 1)}>  
        Click me  
      </button>  
    </div>  
  );  
}  
export default CounterExample;  

Others important built-in hooks:
useContext
useReducer
useCallback
useMemo
useRef

SPA & Routing

SPA stands for Single Page Application. It is a very common way of programming websites these days. The idea is that the website loads all the HTML/JS the first time you visit. When you then navigate, the browser will only rerender the content without refreshing the website.

React-Router

In traditional websites, the browser requests a document from a web server, downloads and evaluates CSS and JavaScript assets, and renders the HTML sent from the server. When the user clicks a link, it starts the process all over again for a new page.

Client side routing allows your app to update the URL from a link click without making another request for another document from the server. Instead, your app can immediately render some new UI and make data requests with fetch to update the page with new information.

React-router-dom package enables faster user experiences because the browser doesn't need to request an entirely new document or re-evaluate CSS and JavaScript assets for the next page. It also enables more dynamic user experiences with things like animation.

npm install react-router-dom

Visualization:
https://remix.run/_docs/routing

Let's a odd react-router to our application clinic

  • First, let's install the package npm i react-router-dom
  • Add the following code to index.js:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import Patients from './components/FPatients';
import History from './pages/history';
import {
  createBrowserRouter,
  RouterProvider,
  Route
} from 'react-router-dom'
import Layout from './pages/layout';

const router = createBrowserRouter([
  {
    path:'/',
    element:<Layout/>,
    children:[
          {
            path:'patients',
            element:<Patients/>
          },
          {
            path:'history',
            element:<History/>
          }
    ]
  }
]);
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RouterProvider router={router}/>
  </React.StrictMode>
);

// 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();

We want the patients, history pages to render inside of the <App> layout. so we need layout components that will stay shown in each nested component, for example 'Sidebar'.

  • create layout at pages/layout.js:
import { Outlet ,Link} from 'react-router-dom';

export default function Layout() {
    return (
      <>
        <div id="sidebar">
          <nav>
            <ul>
              <li>
                <Link to={'patients'}>Patients</Link> 
              </li>
              <li>
              <Link to={'history'}>History</Link>  

              </li>
            </ul>
          </nav>
        </div>
        <div id="detail">
            <Outlet/>
        </div>
      </>
    );
  }

to define our pages, we have to create routes array and pass it to the createBrowserRouter, each object of this array at least must have the following properties:
path, element.

  • Let's create our pages History, Patients at src/pages/:
    patients
import { useState } from "react"
import PCard from "./PatientCard";

const PATIENTS = [
    {
        id:1,
        full_name:"Ali Ahmed",
        birth_date:"10/10/1999",
        gender:"m",
        phone:"+96477889654"
    },
    {
        id:2,
        full_name:"Ameer Saad",
        birth_date:"10/10/2000",
        gender:"m",
        phone:"+96477809654"
    },
    {
        id:3,
        full_name:"Muna Ali",
        birth_date:"10/10/1998",
        gender:"f",
        phone:"+964777809654"
    }
];
const Patients =  (props)=>{
    const [patients, setPatients] = useState(PATIENTS)
    const [fullName , setFullName] = useState('')
    const [gender, setGender] = useState('m')
    const addNew = ()=>{
        if(!fullName)
        {
            alert('Please fill your name')
            return;
        }
        let pTemp = !patients?[]:patients
        pTemp.push({
            id:4,
                    full_name:fullName,
                    birth_date:"10/10/1999",
                    gender:gender,
                    phone:"+964777809604"
        })

        setPatients([...pTemp])
    }

    const changeFullName = (e)=>{
        let v = e.target.value
        console.log(v)
        setFullName(v)
    }


    const genderOnChange = (e)=>{
        setGender(e.target.value)
    }
    const submit = (e)=>{
        e.preventDefault()
        addNew()
    }
    return <div>
        
    
    <form onSubmit={submit}>
        <input value={fullName} onChange={changeFullName} type="text" placeholder="Full Name"/>
        <select value={gender} onChange={genderOnChange}>
            <option value='m'>Male</option>
            <option value="f">Female</option>
        </select>
        <input type="submit"/>
    </form>
    {
        patients?patients.map((item,index)=>{
            return <PCard key={index} data={item}/>
           })
           :
           <p>No Data</p>
    }
</div>
}

export default Patients;

history

const History = ()=>{
    return <p>History Page</p>
}

export default History;

Now run the application should start and you can navigate from page to another inside the layout.

  • Let us add Active link styling:
    Now that we have a bunch of records, it is not clear which one we are looking at in the sidebar. We can use NavLink to fix this.
<NavLink 
                className={({ isActive, isPending }) =>
                isActive
                  ? "active"
                  : isPending
                  ? "pending"
                  : ""
              }
                to={'patients'}>Patients</NavLink> 
              </li>
              <li>
              <NavLink 
  • Let us add new page to view a patient details, we need to pass id of the patient as parameter in the URL to this page, first we need to define a new route object in routes array:
{
            path:"patients/:id",
            element:<Patient/>
}

as you see, we use this pattern /:id, colon to inform the router that there will be a parameter passing that destination page have to receive.

pages/patient.js

import { useParams } from "react-router-dom"

const Patient= ()=>{
    const pars = useParams()
    return <p>Hi, {pars.id}</p>
}

export default Patient;

to access passed parameter via URL, we can use useParams hooks function.

Now we can test our new changes and it should work as expected.

Component Composition

Component composition is the name for passing components as props to other components, thus creating new components with other components.

Example:
Define Button component:

const Button = ({ onClick, children }) => (
 <button onClick={onClick}>{children}</button>
);

Use Button Component as wrapper for other children:

const App = () => {
  const onClick = () => alert('Hey 👋');

  return (
    <Button onClick={onClick}>
         <img src="/logos/logo.svg" />
      Click me!
    </Button>
  );
};

Composition help us to solve prop drilling problem and enhance the performance.
Prop Drilliing:
Prop drilling is the act of passing props through multiple layers of components.

Context

Context provides a way to pass data through the component tree without having to pass props down manually at every level.

In a typical React application, data is passed top-down (parent to child) via props, but such usage can be cumbersome for certain types of props (e.g. locale preference, UI theme) that are required by many components within an application. Context provides a way to share values like these between components without having to explicitly pass a prop through every level of the tree.

Example:
Pass theme mode as props through 3 layers so ButtonThemed can use it

class App extends React.Component {
  render() {
    return <Toolbar theme="dark" />;
  }
}

function Toolbar(props) {
  // The Toolbar component must take an extra "theme" prop
  // and pass it to the ThemedButton. This can become painful
  // if every single button in the app needs to know the theme
  // because it would have to be passed through all components.
  return (
    <div>
      <ThemedButton theme={props.theme} />
    </div>
  );
}

class ThemedButton extends React.Component {
  render() {
    return <Button theme={this.props.theme} />;
  }
}    

Using context, we can avoid passing props through intermediate elements:

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
    // Use a Provider to pass the current theme to the tree below.
    // Any component can read it, no matter how deep it is.
    // In this example, we're passing "dark" as the current value.
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar() {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

class ThemedButton extends React.Component {
  // Assign a contextType to read the current theme context.
  // React will find the closest theme Provider above and use its value.
  // In this example, the current theme is "dark".
  static contextType = ThemeContext;
  render() {
    return <Button theme={this.context} />;
  }
}

API

React.createContext
const MyContext = React.createContext(defaultValue);

Context.Provider
<MyContext.Provider value={/* some value */}>

Every Context object comes with a Provider React component that allows consuming components to subscribe to context changes.

The Provider component accepts a value prop to be passed to consuming components that are descendants of this Provider. One Provider can be connected to many consumers. Providers can be nested to override values deeper within the tree.

All consumers that are descendants of a Provider will re-render whenever the Provider’s value prop changes.

Context.Consumer
Children components (subscribed) act as consumer, to access context provided by the provider:
Class Component

<MyContext.Consumer>
  {value => /* render something based on the context value */}
</MyContext.Consumer>

Function Component:

const context = React.useContext(MyContext);

Theme provider for Clinic App

Let's add functionality of Theme switching using Context API.

  • Create new directory called contexts
  • Create sub-directory called ThemeContext.js
  • Write context provider code:
import {createContext} from 'react'

export const THEMES = {
    dark:{
        background:'black'
    },
    light:{
        background:'blue'
    }
}
const ThemeContext = createContext(THEMES.light)
const ThemeProvider = ThemeContext.Provider;
export  {ThemeContext,ThemeProvider};

Access context from the consumers components:

import { ThemeContext } from "../contexts/ThemeContext/themeContext";
...
    
const context = useContext(ThemeContext)
...
    
 <input style={{
            backgroundColor:context.background
        }} type="submit"/>

Now apply the context provider:

root.render(
  <React.StrictMode>
    <ThemeProvider value={THEMES.light}>
        <RouterProvider router={router}/>
    </ThemeProvider>
  
    
  </React.StrictMode>
);

Now you can print the context object at your console to see that consumers access the values provided from the provider and shared easily without passing props from parent to deep child.

But what if we want to manage the state with the provider? let's optimize the the code design:
let's create a state themeValue inside the provider:

import {createContext,useState} from 'react'

export const THEMES = {
    dark:{
        background:'black'
    },
    light:{
        background:'blue'
    }
}
const defaultSettings = {
    theme:THEMES.light
}
const ThemeContext = createContext(THEMES.light)

export const ThemeProvider = ({theme, children})=>{
    console.log('default passed',theme)
    const [themeValue, setThemeValue] = useState(theme|defaultSettings)

    
    return (
        <ThemeContext.Provider value={{
            settings:themeValue
        }}>
            {children}
        </ThemeContext.Provider>
    )
};  

//const ThemeProvider = ThemeContext.Provider;
export default ThemeContext;

Wrap our application with provider:

import {THEMES} from './contexts/ThemeContext/themeContext'

root.render(
  <React.StrictMode>
    <ThemeProvider theme={THEMES.light}>
      <RouterProvider router={router}/>
    </ThemeProvider>
  
    
  </React.StrictMode>
);

Now we can still access the state inside any child component like patients.js:

import ThemeContext from "../contexts/ThemeContext/themeContext";
const context = useContext(ThemeContext)
const backgroundColor = context.background;
...
    
 <input style={{
            backgroundColor:backgroundColor
        }} type="submit"/>
    

Good!, Let's add new component to toggle the theme and apply some css on the component so we can see how our application will work:\

First let's apply some css on the components:

  • Sidebar: create a sidebar component in components directory and create a new file called sidebar.jsx
import { NavLink } from "react-router-dom/dist/umd/react-router-dom.development"

export const Sidebar = ()=>{
    return (<div className="flex flex-col h-screen p-3 bg-white shadow w-60">
    <div className="space-y-3">
        <div className="flex items-center">
            <h2 className="text-xl font-bold">Dashboard</h2>
        </div>
        <div className="flex-1">
            <ul className="pt-2 pb-4 space-y-1 text-sm">
                <li className="rounded-sm h-8">
                <NavLink 
                         className={({isActive, isPending})=>{
                            let st = isActive?"active":isPending?"pending":"";
                           return st+' '+'p-1 rounded-lg';
                       }}
                        to={'patients'}>
                            Patients
                        </NavLink>
                </li>
                <li className="rounded-sm  h-8">
                <NavLink 
                     className={({isActive, isPending})=>{
                         let st = isActive?"active":isPending?"pending":"";
                        return st+' '+'p-1 rounded-lg';
                    }}
                    to={'history'}>
                            History
                        </NavLink>
                </li>
                
            </ul>
        </div>
    </div>
</div>)

}    
  • App.css
.bg-pink{
  background-color: pink;
}

.active{
  background-color: #61dafb;
}

  • Layout.js
import { Outlet } from "react-router-dom/dist/umd/react-router-dom.development";
import { Sidebar } from "../components/Sidebar";

export default function Layout(){
    return <>
        <header className='fixed top-0 left-0 right-0 h-12 bg-red-300 m-0'>

        </header>
       <div className="flex flex-row mt-12">
       <Sidebar/>
        <div id="detiails" className="w-full p-8">
            <Outlet/>
        </div>
       </div>
    </>
}
  • Let's add switch component to enable user switch theme mode:
    we will install a package react-switch, to make our practicing easy:
npm i react-switch

create switcher.jsx component at components and add the following:

import ThemeContext from '../contexts/ThemeContext/themeContext'
import {useContext} from 'react'
import Switch from 'react-switch'
export const Switcher = ()=>{
    const context = useContext(ThemeContext)
    console.log('c in switcher',context)

    return  <label>
    <span>{context.settings.themeValue.name}</span>
    <Switch onChange={context.actions.onSwitcherChange} checked={context.settings.checked} />
  </label>
}

As you see displayed value changable depends on selected theme, also to toggle switch component we have to manage a boolean state value.

Let's (add states, edit THEMES object, define call back action for switch change event, and expose all these to consumers) to the ThemeProvider context so our switch component can access:

import {createContext,useState} from 'react'

export const THEMES = {
    dark:{
        name:"Dark",	
        background:'bg-zinc-900	'
    },
    light:{
        name:"Light",
        background:'bg-sky-400'
    }
}
const defaultSettings = {
    theme:THEMES.light
}
const ThemeContext = createContext(THEMES.light)

export const ThemeProvider = ({theme, children})=>{
    const [themeValue, setThemeValue] = useState(theme)
    const [checked, setChecked] = useState(theme.name=='Light'?true:false)// true light, false is dark
    console.log('default passed',theme)
    console.log('set to ',themeValue)

    const onSwitcherChange = (e)=>{
        console.log(e)
        setChecked(!checked)
        setThemeValue(e?THEMES.light:THEMES.dark)
        console.log(themeValue)
    }
    return (
        <ThemeContext.Provider value={{
            settings:{
                themeValue,
                checked
                },
            actions:{
                onSwitcherChange
            }
        }}>
            {children}
        </ThemeContext.Provider>
    )
};  

//const ThemeProvider = ThemeContext.Provider;
export default ThemeContext;

onSwitcherChange listen to change event on switch component and update the state so render function rendering again with new values passing to the consumers.

Also let's change background color of the header and sidebar, so they are changing depends on selected theme:
sidebar.jsx:

import { useContext } from "react"
import { NavLink } from "react-router-dom/dist/umd/react-router-dom.development"
import ThemeContext from "../contexts/ThemeContext/themeContext"

export const Sidebar = ()=>{
    const context = useContext(ThemeContext)
    let color = context.settings.themeValue.background
    return (<div className={`flex flex-col h-screen p-3 shadow w-60 ${color}`}>
    <div className="space-y-3">
        <div className="flex items-center">
            <h2 className="text-xl font-bold">Dashboard</h2>
        </div>
        <div className="flex-1">
            <ul className="pt-2 pb-4 space-y-1 text-sm">
                <li className="rounded-sm h-8">
                <NavLink 
                         className={({isActive, isPending})=>{
                            let st = isActive?"active":isPending?"pending":"";
                           return st+' '+'p-1 rounded-lg';
                       }}
                        to={'patients'}>
                            Patients
                        </NavLink>
                </li>
                <li className="rounded-sm  h-8">
                <NavLink 
                     className={({isActive, isPending})=>{
                         let st = isActive?"active":isPending?"pending":"";
                        return st+' '+'p-1 rounded-lg';
                    }}
                    to={'history'}>
                            History
                        </NavLink>
                </li>
                
            </ul>
        </div>
    </div>
</div>)

}

layout.jsx

import { Outlet } from "react-router-dom/dist/umd/react-router-dom.development";
import { Sidebar } from "../components/Sidebar";
import { Switcher } from "../components/switcher";
import ThemeContext from "../contexts/ThemeContext/themeContext";
import {useContext} from 'react'
export default function Layout(){
    const context = useContext(ThemeContext)
    const color = context.settings.themeValue.background;
    console.log('color', color)
    return <>
        <header className={`fixed top-0 left-0 right-0 h-12 m-0 ${color}`}>
            <Switcher/>
        </header>
       <div className="flex flex-row mt-12">
       <Sidebar/>
        <div id="detiails" className="w-full p-8">
            <Outlet/>
        </div>
       </div>
    </>
}

Now as you see, you can toggle the theme from the switch and sidebar and header background will change according to the updates on provider state.

Tailwind CSS

Tailwind CSS, as per their own website is a "utility-first CSS framework" which provides several of these opinionated, single-purpose utility classes that you can use directly inside your markup to design an element.

Installation

It is prefer to follow their official steps:
https://tailwindcss.com/docs/guides/create-react-app

Help:

  • Append command postcss src/App.css -o public/output.css to the script in package.json file:
"scripts": {
    "start": "react-scripts start && postcss src/App.css -o public/output.css",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },

and in the public/index.html, link output.css:

    <link href="%PUBLIC_URL%output.css" rel="stylesheet">

Task

References

https://reactrouter.com/
https://tailwindcss.com/
https://reactjs.org/docs/
https://www.npmjs.com/package/react-switch