😎React Js (Udemy - John Smilga)

https://www.youtube.com/watch?v=4UZrsTqkcW4&t=38s

What is create-react-app

create-react-app is an npm package that helps to create react apps.

Creating the react apps from scratch can be a long process because we need to

  • configure it

  • setup web pack and babel ( that packages our code and transforms it into browser understandable code)

  • create react app also includes (otherwise we need to configure ) web server

  • create react app also includes testing library (which we can't easily implement it ourselves).

Hence, create-react-app provides out of box configuration for all the above things.

What is npx?

We will be using npx with create-react-app. The npx is Node Package Runner, it

  • Downloads the latest version of create-react-app template every time

  • So it's not dependant on when you last installed create-react-app

  • In fact, the create-react-app is never installed on our machines, the npx pulls the latest version of create-react-app

  • npx comes with npm 5.2 or higher

Starting steps after create-react-app

Delete all the files in the src folder except index.js

How to connect index.js (starting point of the app) with index.html?

index.html will have the code

 // index.html
   <div id="root"></div>

Our entry point to the app isindex.js because the goal is to insert index.js code into the above div placed at index.html.

// index.js
import React from 'react'

function Greeting() {
  return <h1>This is Sandeep, and this is my first component</h1>
}
// something must be returned

Notice that the first letter of the function should be captialized. Here Greeting, G is capital.

Also, we must always return something from the function. Otherwise, we get an error.

How to insert the index.js into <div id="root"></div>. That is by using ReactDom.render()

//index.js
import React from 'react'
import ReactDom from 'react-dom'
 
function Greeting() {
  return <h1>This is Sandeep, and this is my first component</h1>
}
ReactDom.render(<Greeting />, document.getElementById("root")) // inserts this component into index.html div

JSX

It's a syntactical sugar to make the elements look like HTML but under the hood, it uses React.createElement(). The syntactic difference between HTML and JSX is, in JSX we use camel case, so for example, onclick in HTML will be onClick is JSX. Also, class in HTML will be className in JSX.

return <h1>This is Sandeep, and this is my first component</h1>
// this is same as

return React.createElement('h1', {}, "Hello World"); // 2nd param is for props

// this becomes so difficult with nested components like
 
<div>
 <h1>This is nested</h1>
</div>

React Fragments

return (
    <div>Hello</div>
    <h1>Hello again</h1> 
)

// The above is not possible because the return should have only one element. 
// Hence we can wrap everything in a div but that extra div might not what we want.
// Without creating extra div, if we want to achieve this we can use react fragements


return (
    <React.Fragment>
        <div>Hello</div>
        <h1>Hello again</h1> 
    </React.Fragment>
)


// shortcut to fragment

return (
    <>
        <div>Hello</div>
        <h1>Hello again</h1> 
    </>
)

The idea of splitting as components

Let's say we have the below code that has a h1 and p. We can split into two componets.

import React from "react";
import ReactDom from "react-dom";

function Greeting() {
  return (
    <div>
      <h1>Hello there,</h1>
      <p>This is John Doe</p>
    </div>
  )

}

// We can make a separate component for p

Separate component for p

function Greeting() { // component 1
  return (
    <div>
      <h1>Hello there,</h1>
      <Person /> {/* Component 2 being used  */}
      
    </div>
  )

}

const Person = () => <p>This is John Doe</p> // component 2

Practical Tasks

Task 1 : (use state on Array) Remove List Items on click

clear items deletes all and Remove deletes individual item
import React from 'react';
import { data } from '../../../data';

const UseStateArray = () => {
  const [people, setPeople] = React.useState(data)

  const removeItem = (id) => {
    let filteredData = people.filter(person => person.id !== id)
    console.log(filteredData)
    setPeople(filteredData)
  }
  return <>
    { people.map(person => {
      const { id, name } = person
      return (<div key={id} className="item">
        <h4>{name}</h4>
        <button className="btn" onClick={() => removeItem(id)}>Remove</button>
      </div>

      )
    })}

    <button className="btn" onClick={() => setPeople([])}>Clear Items</button>
  </>
};

export default UseStateArray;

Task 2 : (use state on Object) Change the message property in the object

Change Message click changes message to Hello World
import React, { useState } from 'react';

const UseStateObject = () => {
  const [person, setPerson] = useState({ name: 'Peter', age: 24, messsage: 'random message' })
  console.log(person)
  const changeMessage = () => {
     // first copy the object and then change the property you want
    setPerson({ ...person, messsage: "Hello world" })
  }
  return <>
    <h3>{person.name}</h3>
    <h3>{person.age}</h3>
    <h3>{person.messsage}</h3>
    <button className="btn" onClick={changeMessage}>Change Message</button>
  </>
};

export default UseStateObject;

Task 3 : (use State on numbers) - Bad practice as we are not using callback to update state. Good practice is given below

import React, { useState } from 'react';

const UseStateCounter = () => {
  const [value, setValue] = useState(0)
  const reset = () => {
    setValue(0)
  }
  return <>
    <section style={{ margin: '4rem 0' }}>
      <h2>Regular counter</h2>
      <h1>{value}</h1>
      <button className="btn" onClick={() => setValue(value - 1)}>decrease</button>
      <button className="btn" onClick={reset}>Reset</button>
      <button className="btn" onClick={() => setValue(value + 1)} > increase</button>
    </section>
  </>
};

export default UseStateCounter;

Task 3 : (Good practice) Why to pass a callback function to update the state?

The above counter example (Task 3) is a bad practice. Let's say we have clicked on increase button but the value increases only after 2 seconds. In this two seconds, how many ever times we press the increase button, the value updates only once after 2 seconds. This is because setValue() is asynchronous and when it get's executed, it takes the global value (at the time of execution). This will be 0 and after 2 seconds, when async function, setValue gets executed this will be looking for global value which is 0 and updates to 1. That's the reason it ignores all the clicks.

The best practice is to use a callback function inside the setValue and pass this value as an argument. Now, the value is bound to the function and always updates internally when clicked and will be displayed after 2 seconds.

  // Bad practice
  const complexIncrease = () => {
    setTimeout(() => {
      setValue(value + 1) // bad because we are using global value that doesn't get updated until 2 seconds
    }, 2000)
  }

// Good practice, making the global value available inside the async function setValue
  const complexIncrease = () => {
    setTimeout(() => {
      setValue((preVal) => preVal + 1)
    }, 2000)
  }

<button className="btn" onClick={complexIncrease} > increase</button>

This above approach of using callbacks to get the value inside of useState can be applied for all 3 tasks above. Some times, when using numbers as state, we definitely need it but in case of array and object useState, it's optional.

UseEffect hook

useEffect runs after every render by default. Every time the state changes the component rerenders and hence the useEffect runs by default.

useEffect by default (no second argumnet)
useEffect by default (no second argumnet)
useEffect if condition (default useEffect just like above)

UseEffect with second argument empty

The useEffect runs only once (first time) when second argument array is empty

UseEffect with dependencies

If we have dependencies in second argument array, then the useEffect runs when that dependency/dependencies change

useEffect dependecies

We can also have multiple useEffects

multiple useEffect hooks

useEffect with clearnup function

Why we need a cleanup function
useEffect cleanup function

Task 4 : (useEffect) Build a github user profiles page

Task
import React, { useState, useEffect } from 'react';

const url = 'https://api.github.com/users';

const UseEffectFetchData = () => {
  const [users, setUsers] = useState([]);

  const getUsers = async () => {
    const response = await fetch(url)
    const users = await response.json()
    setUsers(users)
  }

  useEffect(() => {
    getUsers()
  }, [users]) // if you omit the dependency array, then you get infinite loop as setUsers() will trigger a re-render always
  return <>
    <h3>Github users</h3>
    <ul className="users">
      {users.map(user => {
        const { id, login, avatar_url, html_url } = user
        return <li key={id}>
          <img src={avatar_url} alt={login} />
          <div>
            <h4>{login}</h4>
            <a href={html_url}>profile</a>
          </div>
        </li>
      })}
    </ul>
  </>;
};

export default UseEffectFetchData;

Task 5: Set Loading Or Error before displaying data

import React, { useState, useEffect } from 'react';
const url = 'https://api.github.com/users/QuincyLarson';
const MultipleReturns = () => {

  const [isLoading, setIsLoading] = useState(false)
  const [isError, setIsError] = useState(false)
  const [user, setUser] = useState('')

  // const getUser = async () => {
  //   const response = await fetch(url)
  //   const user = await response.json()
  //   setUser(user)
  //   console.log(user)
  // }


  useEffect(() => {
    // getUser() // one way you can do or you can use then and catch like below
    
    setIsLoading(true)
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setUser(data.login)
        setIsLoading(false)
        setIsError(false)
      })
      .catch(error => {
        console.log(error)
        setIsError(true)
      })
  }, [])

  if (isLoading) {
    return <div>
      <h3>Loading...</h3>
    </div>
  }

  if (isError) {
    return <div>
      <h3>Error...</h3>
    </div>
  }

  return <>
    <h1>{user}</h1>
  </>;
};

export default MultipleReturns;



//2nd way
  const [isLoading, setIsLoading] = useState(true)

useEffect(() => {

// OMIT THE setLoading(true) here
    fetch(url)
      .then(res => res.json())
      .then(data => {
        setUser(data.login)
        setIsLoading(false)
        setIsError(false)
      })
      .catch(error => {
        console.log(error)
        setIsError(true)
      })
  }, [])

  if (isLoading) {
    return <div>
      <h3>Loading...</h3>
    </div>
  }

&& and || operators

Example of && and ||

An alternate way of writing || using ! and &&

Alternate way of writing || using &&

Problems with Fetch API - Reasons to use axios

  • We need to call then twice (one to get json response and the other one is to get the data). That is because the response.json() gives you the promise and not the data

    • fetch(url)
            .then(res => res.json()) // 1st then to get json which gives promise
            .then(data => { // 2nd then to get the data
              setUser(data.login)
              setIsLoading(false)
              setIsError(false)
            })
            .catch(error => {
              console.log(error)
              setIsError(true)
            })
        }
  • The second problem is the fetch doesn't return 404 error if the user not found. If you modify the URL, then it doesn't consider 404 as an error. We need to manually write a case to check if we get back the data. The network errors are considered as the errors by fetch API and not 404.

    • const url = 'https://api.github.com/users/QuincyLarson';
      
      fetch(url).then().catch(err=>clg(err)
      
      // If I modify the URL to 'https://api.github.com/users/QuincyLarsons', it 
      // doesn't catch the error. We expect to get 404 (page not found) error but 
      // it doesn't throw that error. 
      
      // Hence we need to modify our then block a bit to handle this

      The solution to this problem

    • fetch(url)
            .then(res => {
              console.log(res)
              if (res.status >= 200 && res.status <= 299) { // this is the check we implement
                return res.json()
              }
              else {
                setIsLoading(false)
                setIsError(true)
                // throw new Error("Something went wrong")
                throw new Error(res.statusText)
              }
            })
            .then(data => {
              setUser(data.login)
              setIsLoading(false)
              setIsError(false)
            })
            .catch(error => {
              console.log(error)
              setIsError(true)
            })
        }

React Forms

Controlled Inputs (As I type I change the state)

In controlled inputs, we'll link up our inputs to the state values by using event.target.value. We don't use useRef hook here

Uncontrolled Inputs

We use useRef for the uncontrolled inputs and not the event.

controlled and uncontrolled inputs (screenshot from cocktail project of John Smilga)

Advantages of Uncontrolled inputs sometimes

  • If we want to focus on the search bar right away when the page loads then we can do it using useRef. Of course we can use controlled inputs and then use useRef as well but instead, we can only use useRef to set the state as well like shown above in the image (commented part)

htmlFor

Use of htmlFor

Submit button

We have two options

  • Either we can add the onSubmit on the form

  • Or we can add onClick on to the button inside the form and make the button type = submit

// OPTION 1 - onSubmit on the form

function BookList() { // component 1

  const handleSubmit = () => {
    alert("Form submitted")
  }
  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input type="text" id="firstName" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input type="text" id="email" />
      </div>
      <button type="submit">Submit</button> // we can omit this but it's good practice to have this when we have multiple buttons in the form
    </form>
  )

}


// OPTION 2

<form>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input type="text" id="firstName" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input type="text" id="email" />
      </div>
      <button onClick={handleSubmit}>Submit</button>
</form>

Just like in JavaScript, in the form handler method, we get access to event by default

 const handleSubmit = (e) => { //have access to e by default
    alert("Form submitted")
  }

By default, when the form is submitted, it refreshes the page. We need to prevent this by using e.preventDefault().

Now, how to access the data inside the inputs

Well, we can set the state values for each input and access them. But when we type something, the state value should be updated using onChange. As and when we type, the state value changes using onChange where the state value gets updated. This is why we call them controlled inputs where the state value is changed based on what we type when onChange is used. onChange is controlling the state updates.

const [firstname, setFirstname] = useState('')
  const [email, setEmail] = useState('')
  const handleSubmit = (e) => {
    e.preventDefault()
    alert("Form submitted")
  }
  return (
    <form>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input onChange={(e) => setFirstname(e.target.value)} value={firstname} type="text" id="firstName" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input onChange={(e) => setEmail(e.target.value)} value={email} type="text" id="email" />
      </div>
      <button onClick={handleSubmit}>Submit</button>
    </form>
  )

Now that we connected the dots, let's work on a simple task

Task 6: Add form data to an array and display it

import React, { useState } from "react";
import ReactDom from "react-dom";
import "./index.css"

function People() { // component 1

  const [firstname, setFirstname] = useState('')
  const [email, setEmail] = useState('')
  const [people, setPeople] = useState([])
  const handleSubmit = (e) => {
    e.preventDefault()
    // 1st way
    // setPeople([...people, { firstname, email }])

    //2nd way
    setPeople((prevPeople) => [...prevPeople, { firstname, email }])
  }
  return (
    <form>
      <div>
        <label htmlFor="firstName">First Name</label>
        <input onChange={(e) => setFirstname(e.target.value)} value={firstname} type="text" id="firstName" />
      </div>
      <div>
        <label htmlFor="email">Email</label>
        <input onChange={(e) => setEmail(e.target.value)} value={email} type="text" id="email" />
      </div>
      <button onClick={handleSubmit}>Submit</button>
    </form>
  )

}
ReactDom.render(<People />, document.getElementById("root"));

Handling multiple inputs

You see we're setting two useStates to two values, one for name and one for age. What if there are multiple inputs like more than 7 or 8 in the form which will generally be the case. We could technically have those many useStates but it would be better if we manage a single useState using an object.

Finally, we can make use of name attribute in the jsx

Before we see how to handle multiple inputs, let's get to know what name attribute in HTML input tag does

The name attribute indicates the name of the input. Let's say we have 10 input elements so when we give a unique name to each one, we can then use this later to identify which input we are talking about. The event (in handleSubmit) will have access to the name using e.target.name. That's the beauty. Let's now see how we can handle multiple inputs.

import React, { useState } from "react";
import ReactDom from "react-dom";
import "./index.css"


let id = 0;
function People() { // component 1

  // First Way of having each useState for each field

  // const [firstname, setFirstname] = useState('')
  // const [email, setEmail] = useState('')
  // const [age,setAge] = useState('')

  // Best way of having a single useState for all the fields

  const [person, setPerson] = useState({ firstName: '', email: '', age: '' }) // see we're setting an object to hold multiple values



  const [people, setPeople] = useState([])

  const handleSubmit = (e) => {
    e.preventDefault()    
    setPeople((prevPeople) => [...prevPeople, { ...person, id: ++id }])
  }

  const handleChange = (e) => {
    const name = e.target.name // this will be the name of the input element we type in. For example, if we are typing in first name input then the name will be firstName
    const value = e.target.value // this will be the value of the input
    console.log(name, value)
    setPerson({ ...person, [name]: value }) // fist copying person object and then updating the name value dynamically.
  }
  return (

    <article>
      <form>
        <div>
          <label htmlFor="firstName">First Name</label>
          <input name="firstName" onChange={handleChange} value={person.firstname} type="text" id="firstName" />
        </div>
        <div>
          <label htmlFor="email">Email</label>
          <input name="email" onChange={handleChange} value={person.email} type="text" id="email" />
        </div>
        <div>
          <label htmlFor="age">Age</label>
          <input name="age" onChange={handleChange} value={person.age} type="text" id="age" />
        </div>
        <button onClick={handleSubmit}>Submit</button>
      </form>
      {people.map(person => {
        return <h1 key={person.id}> {person.id}, {person.firstName}, {person.email}, {person.age}</h1>
      })}
    </article>
  )

}
ReactDom.render(<People />, document.getElementById("root"));

useRef hook - Uncontrolled inpyts

In uncontrolled inputs, we'll not link up our inputs to the state values.

Though we could do many things with useRef, the most popular one is to target the dom element and set up uncontrolled input similar to how we do in vanilla JS

useRef works a lot like useState but there are a few differences.

Though the useRef and useState both preserves the values between the renders, the useRef doesn't cause a re-render like useState. One of the most use cases of useRef is targeting the dom element.

Think like we are getting back the HTML element on a form submit just like we get when we do document.querySelector(). You can then get the value or whatever. Imagine document.querySelector() is same as useRef current.

useRef
import React, { useRef } from "react";
import ReactDom from "react-dom";
import "./index.css"


function People() {

  const refContainer = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(refContainer.current.value)
  }
  return (

    <article>
      <form>
        <input type="text" ref={refContainer} />
        <button type="submit" onClick={handleSubmit}>Submit</button>
      </form>

    </article>
  )

}
ReactDom.render(<People />, document.getElementById("root"));

useRef is mostly used to focus on the input element when the page loads

Task 7 : Focus on input when form loads using useRef

import React, { useRef } from "react";
import ReactDom from "react-dom";
import "./index.css"


function People() {

  const refContainer = useRef(null)

  const handleSubmit = (e) => {
    e.preventDefault()
  }
  React.useEffect(() => {
    refContainer.current.focus()
  }, [])
  return (

    <article>
      <form>
        <input type="text" />
        <input type="text" />
        {/* the below one will be focused */}
        <input type="text" ref={refContainer} /> 
        <button type="submit" onClick={handleSubmit}>Submit</button>
      </form>

    </article>
  )

}
ReactDom.render(<People />, document.getElementById("root"));

2nd use case of useRef

To display the previous state value

2nd usecase of useRef
more on useRef

useReducer hook

Similar to useState (in fact, the useState is built on useReducer) that helps to set the state and we can use this instead of useState when the app grows big and we need a certain way to update the state. Consider having many state values that need to change on updating something then this is the way to go. If I click on a button and if 10 individual setState calls must be done then useReducer is better than useState.

Let's use useReducer to replicate this. Though the useReducer is not necessary here with such a small number of states, I would like to show how useReducer works.

  • First define useReducer that takes two args, reducer and intital state

  • The useReducer will give back state and dispatch (can be any names). Destructure them

  • Define the arguments of useReducer which are reducer and intitial state

// outside component
const initState = {
  people: data,
  isModalOpen: true,
  modalContent: 'hello world'
}

const reducer = (state = initState, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        modalContent: 'Item added',
        isModalOpen: true,
        people: [...state.people, action.payload]
      }
    default:
      throw new Error("No matching action type") // throwing error is better so that we don't use wrong dispatch 
  }
}

// inside component

//define useReducer
const [state, dispatch] = useReducer(reducer, defaultState)


  const handleSubmit = (e) => {
    e.preventDefault();
    if (name) {
      const newItem = { id: new Date().getTime().toString(), name }
      dispatch({ type: 'ADD_ITEM', payload: newItem }) // DISPATCH THE ACTION
    } else {
      dispatch({ type: 'RANDOM' }) // DISPATCH THE ACTION - throws an error as random is not defined
    }
  }

Add, remove operations of useReducer

import React, { useState, useEffect, useRef, useReducer } from "react";
import ReactDom from "react-dom";
import "./index.css"
const data = [{ id: 1, name: 'Sandeep' }, { id: 2, name: 'Vijay' }]
const styles = {
  form: {
    margin: '2rem 4rem',
    width: '50%',
    button: {
      color: 'white',
      backgroundColor: 'black',
      padding: '.3rem',
      margin: '2rem'
    }
  }
}
const defaultState = {
  people: data,
  isModalOpen: true,
  modalContent: 'hello world'
}


// REDUCER PART
const reducer = (state, action) => {
  switch (action.type) {
    case 'ADD_ITEM':
      return {
        ...state,
        modalContent: 'Item added',
        isModalOpen: true,
        people: [...state.people, action.payload]
      }
    case 'NO_VALUE_ITEM':
      return {
        ...state,
        modalContent: 'Please provide a value',
        isModalOpen: true,
      }
    case 'CLOSE_MODAL':
      return {
        ...state,
        isModalOpen: false
      }
    case 'REMOVE_ITEM':
      return {
        ...state,
        people: state.people.filter(person => person.id !== action.payload),
        modalContent: 'Removed Item',
        isModalOpen: true
      }
    default:
      throw new Error("No matching action type")
  }
}


function People() {
  const [name, setName] = useState('')
  const [people, setPeople] = useState(data)
  const [showModal, setShowModal] = useState(false)
  const [state, dispatch] = useReducer(reducer, defaultState)



  const handleSubmit = (e) => {
    e.preventDefault();
    if (name) {
      const newItem = { id: new Date().getTime().toString(), name }
      dispatch({ type: 'ADD_ITEM', payload: newItem })
    } else {
      dispatch({ type: 'NO_VALUE_ITEM' })
    }
  }
  const closeModal = () => {
    dispatch({ type: 'CLOSE_MODAL' })
  }
  return <>
    {state.isModalOpen && <Modal closeModal={closeModal} modalContent={state.modalContent} />}
    <form style={styles.form} onSubmit={handleSubmit}>
      <div>
        <input type="text" value={name} onChange={({ target }) => setName(target.value)} />
      </div>
      <button style={styles.form.button} type="submit"> Add</button>
    </form>
    {state.people.map(person => {
      return <div key={person.id}>
        <h4>{person.name}</h4>
        <button onClick={() => dispatch({ type: 'REMOVE_ITEM', payload: person.id })}>Remove</button>
      </div>
    })}
  </>

}

function Modal({ modalContent, closeModal }) {
  useEffect(() => {
    setTimeout(() => closeModal(), 3000)
  })
  return <div className="modal">
    <p>{modalContent}</p>
  </div>
}

ReactDom.render(<People />, document.getElementById("root"));

useContext hook

Used for avoiding props drilling. If intermediate components don't need a specific prop, we some times need to still pass it to that component in order to pass the prop further. We can make use of useContext hook for that

React.createContext() will give access to Provider and Consumer. We earlier used to use Consumer but now we use useContext hook for the consumption of the provided value by Provider.

Provider

Provider

Consumer

Consumer - useContext

Setup errors of workspace

The browser page does not auto-refresh after making any changes in the component

To the root path, add .env file and inside it, add FAST_REFRESH=false

Interesting Articles

Do we need to do event delegation in react?

No, react do performance optimizations for us.

React does perfomance optimizations so we don;'t need to worry

Build and Deploy

Add CI =

Add CI= in package.json

build in package.json (add CI)

Custom hook

Let's say we have a functionality to fetch the data from an API. So it would have these functionality

  • Have state value to store the fetched data

  • An async function to fetch the data from the API and store in the state above

  • useEffect which calls the function above

So useEffect calls the async function --> async function that gets data from external API and stores in the state.

So the full component would look like this

import React, { useState, useEffect } from 'react'
import { useFetch } from './2-useFetch'

const url = 'https://course-api.com/javascript-store-products'

const Example = () => {
  const [loading, setLoading] = useState(true)
  const [products, setProducts] = useState([])

  const getProducts = async () => {
    const response = await fetch(url)
    const products = await response.json()
    setProducts(products)
    setLoading(false)
  }

  useEffect(() => {
    getProducts()
  }, [url])

  console.log(products)
  return (
    <div>
      <h2>{loading ? 'loading...' : 'data'}</h2>
    </div>
  )
}

export default Example

The data fetch part is this one

  const [loading, setLoading] = useState(true)
  const [products, setProducts] = useState([])

  const getProducts = async () => {
    const response = await fetch(url)
    const products = await response.json()
    setProducts(products)
    setLoading(false)
  }

  useEffect(() => {
    getProducts()
  }, [url])

This part would be common to fetch the data. Let's say we have to fetch from other API as well. We then need to repeat this above code (variables would be different).

So to keep the code DRY, we can put the above functionality in a function and call that function every time and reuse. But the problem with that is, it would not re render if data changes. So in this case to reuse the functionality and also re render the useEffect hook when data changes, we can use Custom hook.

The difference between a function and custom hook is

  • Custom hook re-renders any changes just like a useEffect hook

  • It will start with use keyword

So let's turn the above component to use a custom hook called useFetch(). This useFetch takes a URL parameter so that we can make use of this hook to fetch different APIs. This hook will return the things whatever the main component needs.

useFetch.js
import { useState, useEffect } from 'react'

export const useFetch = (url) => {
  const [loading, setLoading] = useState(true)
  const [products, setProducts] = useState([])

  const getProducts = async () => {
    const response = await fetch(url)
    const products = await response.json()
    setProducts(products)
    setLoading(false)
  }

  useEffect(() => {
    getProducts()
  }, [url])

  return { products, loading }
}

The component would now be

Example.js
import React, { useState, useEffect } from 'react'
import { useFetch } from './2-useFetch'

const url = 'https://course-api.com/javascript-store-products'

const Example = () => {
  const { products, loading } = useFetch(url)

  console.log(products)
  return (
    <div>
      <h2>{loading ? 'loading...' : 'data'}</h2>
    </div>
  )
}

export default Example

This could not be achieved with normal function as we get error if we use hooks inside normal function. But one thing to keep in mind is, if the function name starts with capital letter then hooks can be used inside that function. Then that function will become a react component.

To better understand the difference between normal function and react custom hook, refer this

PropTypes

PropTypes are used when there are some props missing in data we get back from server. Let's say we have https://course-api.com/react-prop-types-example, the last product has missing image and price

If this happens, your entire app will fail and will throw error. Even if 99 products are good and if one product has a missing prop, you will get error and app will not load at all. To counter this, we can use PropTypes and the idea is, if some of the props are missing then we can provide default props.

Now I could have explained this in detail like how we implement it, but since this is very rarely used in the code, I would rather refer to John's video on udemy when needed.

Performance Optimization

React is fast by default. But it still provides some hooks so that we can optimize it further in necessary use cases. These hooks are

  • React.memo

  • useMemo

  • useCallback

Just because react provides these, it doesn't mean you need to use them all over the place. They come with a cost of memory and performance, so absolutely if necessary then only use it. Here's an article that explains when and when not to use it

KentCDodds

React Memo

When state or props change in parent, the parent component re-renders. Because of this, even the child component/s re-render which might nor be necessary in some cases.

React memo explained

Wrapped the component in React memo

useCallback

So when count is increased, we didn't want to re-render our child component BigList as the BigList component didn't have count as props. But we do have addToCart function as props in BigList. This function will change every time when count changes as the functions will be re-created when component re-renders hence triggering the child BigList to re-render.

So one reason to use useCallback is to avoid this function (addToCart) re-creation. Once the function doesn't re-create then our react memo will stop re-rendering the child if the parent Index's count value changes.

The second use case of useCallback hook is to address a warning about missing dependencies.

useMemo

useCallback will remember the function (it decides if the function needs to be re-created or not) whereas useMemo is not for a function but for a value. useMemo decides whether a value should be recomputed or not.

Last updated

Was this helpful?