DEV Community

loading...
Cover image for React Hooks

React Hooks

Kevin Odongo
I am here to learn. Technology evolves every day and my passion is to learn and create applications that can change and make human day to day activity simple and organized.
・9 min read

Hey Dev's

If you read my previous tutorials you will note the source of these recent tutorials.

  1. React Tailwind and TypeScript
  2. TypeScript

In this tutorial i will be going through React Hooks. Sharing what i learnt might help someone through there own process. Through the tutorial i will keep it simple with no jargon words just plain English so we can understand the whole concept of each hook.

We are going to discuss the following hooks:

  • useState
  • useEffect
  • useContext
  • useReducer
  • useMemo
  • useRef
  • useMemo
  • useCallback

Let us break this starting with the easiest which is useState.

useState

This hook returns a value and provides a function to update it. In lay man it gives you a place to have your value as well as provides you a function to update that value. Remember this value is not stored permanently and you will loose the value once the component unmounts.

Example

import React from "react"

function App(){
  const [username, setUsername] = React.useState("Kevin Odongo")

  return(<div>{username}</div>)
}
Enter fullscreen mode Exit fullscreen mode

In TypeScript we can go further and declare the type of data we want to store in our state. Let us refactor the above to have some type check. I have also added a function which updates our state.

import React from "react"

function App(){
  // i have added a type check of string or null for the value username
  const [username, setUsername] = React.useState<string | null>("Kevin Odongo")

  // update user
  function updateUser(){
      let value = "John Doe"
      setUsername(value)
  }

  return(<div>{username}</div>)
}

// This can also be achieved by declaring the type like this
// If you went through the TypeScript tutorial you will note that you can use interface or type
interface User {
   name: string
   age: number
   isAdmin: boolean
}

function App(){
   // we have use the types of the user and referenced in this state now our value has checks of what structure it should be.
   const [user, setUser] = React.useState<User>({ name: '', age: '', isAdmin: ''})
   ....
}
Enter fullscreen mode Exit fullscreen mode

Below is how you can use it with input field. In this example when a user types something our state will be updated.

Example

// input
<input onChange={(value) => setUsername(value)} value={username} type="text"></input>
Enter fullscreen mode Exit fullscreen mode

With useState it is quite straight forward. In vue we access our state in the data function found in the script section.

useRef

This returns a mutable ref object whose .current value is indicated in the initialValue. It allows you to access a specific item you have referenced and can access its current state.

Take a look at this example

import React from "react"
function App(){
   const inputRef = React.useRef<any>(null)

  return(<div>
    <input ref={inputRef}></input>
  </div>)

}
Enter fullscreen mode Exit fullscreen mode

Assuming you have an item you want to access when a button is clicked but its somewhere near the footer. You can access its current state by using ref. This hook is also not complicated to know its more straightforward.

Alt Text

You can use ref on other HTML elements. This is just an example to better understand how to work with ref.

useEffect

I found useEffect quite powerful because they combined the previous approach of using componentDidMount...etc. Take it like this if you have a function that you want to run when your component mounts you can use useEffect to handle the whole process. I call it side effect of a function. When our component mounts we have some changes that we want to run for example call an API...etc.

Example
This example fetches users every time the component mounts.

import React from "react"

function App(){
   React.useEffect(() => {
     async function fetchUsers(){
       .... do something
     }
     fetchUsers()
   }, [])
}
Enter fullscreen mode Exit fullscreen mode

useEffect accepts two things a callback and dependencies.

React.useEffect(callback|dependencies)
Enter fullscreen mode Exit fullscreen mode

Our functions either depend on props/state values or does not have any dependencies. We also would like to control how or when our side effects should be called. You can not control how many times React will render a component but you can control your side effects.

  • If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument.

Example
In this example our effect will run when mounted is true with a cleanup when component unmounts. Also note we have declared an empty array that indicates this side effect does not have any dependency within the function. One more thing to note you can use async within the useEffect.

import React from "react"

function App(){
   React.useEffect(() => {
     let mounted = true
     async function fetchUsers(){
       .... do something
     }
     if(mounted)fetchUsers()
     // cleanup
     return(() => {
        mounted = false 
     })
   }, []) // empty array
}
Enter fullscreen mode Exit fullscreen mode
  • If you want to run an effect every time a component re-renders then consider removing the empty array. One thing you should be careful while using effect is that you can easily go into infinite loop.

Example
Note that we have removed the dependency array. Be careful with this scenario.

import React from "react"

function App(){
   React.useEffect(() => {
     async function fetchUsers(){
       .... do something
     }
     fetchUsers()
   }) // removed the dependency array
}
Enter fullscreen mode Exit fullscreen mode
  • If your effect have some dependency you should include all the dependencies in the dependencies array. How do you know your dependencies?. This is a good question that in my opinion i feel every value in the function is a dependent to the function.
import React from "react"

type User {
   name: string
   age: number
   isAdmin: boolean
}

function App(user: User){
   const [isLoading, setisLoading]=React.useState<boolean>(false)

   React.useEffect(() => {
     let mounted = true
     ... logic
     return(() => {
       mounted = false 
    })
   }, [user, isLoading]) // this side effect has user and isLoading as its dependencies.
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

useContext

If you have a basic understanding how state management like Vuex, Redux ...etc, then you will come to appreciate how working with useContext and useReducer can allow you to create a simple state management in your application assuming you do not want to add any third party state management.

We can have a value in our context which can be accessed by any component in our application. As you have learnt you can always pass values through props to different components. The only challenge with props the passage has to be from Parent component to children components.

I found working with useContext quite powerful if your application requires authentication and you want to track the authenticated user between components.

Example
In this example we want to create a simple store with our user attributes and access it in our home component. This will be a simple approach to understand useContext.

// create a folder called store in src folder and add a file called index.tsx

mkdir ./src/store
touch ./src/store/index.tsx

// now let us create a simple store and export our store.
// __/src/store/index.tsx__
export const store = {
   // let us have user in our store
   user: {
     id: 5748-2939-3985-9294
     name: "Kevin Odongo",
     age: 36,
     isAdmin: true
   }
}

// in our app.tsx let us import the store and create a context
// __App.tsx__
import React from "react"
import Routes from "./routes/Routes"
// import store
import { store } from "./store"
// create a context
export const StoreContext: any = React.createContext(store)

function App(){
    // we need to wrap our routes so we can access our context from all components
   return(
      <StoreContext.Provider value={store}>
         <Routes />
     </StoreContext.Provider>
   )
}


// in our components we will access the store as follows
// __Home.tsx__
import React from "react"
import { StoreContext } from "./App"

function(){
   // with this we have access to our store
   const _store: any = React.useContext(StoreContext)
   const [user, setUser] = React.useState<any>("")

   React.useEffect(() => {
     setUser(_store.user)
   }, [_store])

   // we should be able to access the user details
   // imagine we have to use the user id to fetch his details this will be quite a good solution
   return(<div>{user.name}</div>)
}

Enter fullscreen mode Exit fullscreen mode

You can have multiple context in your application.

Example

function App(){
  ...
  // here we have a theme provider and a store provider
  return(
     <Theme.Provider>
       <Store.Provider>
         <Routes />
      </Store.Provide> 
    </Theme.Provider>
  )
}
Enter fullscreen mode Exit fullscreen mode

So how do we update our store?. Good question. We always pass a function down through the context to allow consumers to update the context:

Example

// store
// __/src/store/index.tsx__
export const store = {
    // We have cleared our store assuming we need the authenticated user when the log in and we will update the user details once they log in.
   user: {
     id: "",
     name: "",
     age: "",
     isAdmin: ""
   },
   // this function will update the user details
   setUser: () => {}
}

// in our app.tsx let us import the store and create a context
// __App.tsx__
import React from "react"
import Routes from "./routes/Routes"
// import store
import { store } from "./store"
// create a context
export const StoreContext: any = React.createContext(
  store,
  // we are passing this function to all our consumers to be able to update the store.
 // we will look at another approach while going through useReducer.
)

function App(){
  const [user, setUser] = React.useState<any>("")
  const value = { user, setUser}

  React.useEffect(() => {
     // check if user is authenticated
    //  lets imagine the user is authenticated and we have retrived his details and saved in the state.
     setUser({
       id: 5748-2939-3985-9294
     name: "Kevin Odongo",
     age: 36,
     isAdmin: true
    })
  })
   return(
      <StoreContext.Provider value={value}>
         <Routes />
     </StoreContext.Provider>
   )
}


// in our components we will access the store as follows
// __Home.tsx__
import React from "react"
import { StoreContext } from "./App"

function(){
   // in our home component being the consumer we can assess the store and all its values and function.
  // i have added a button that when you click will update the store to simulate a new user login
   return(
       <StoreContext.Consumer>
            {(store: any) => (
                <div style={{ padding: '10px' }}>
                    <div>{store.user.name}</div>
                    <button onClick={() => store.setUser({
                        name: "Brian Mark",
                        age: 38,
                        isAdmin: true
                    })}>UPDATE USER</button>
                </div>
            )}
        </StoreContext.Consumer>

  )
}
Enter fullscreen mode Exit fullscreen mode

Great we now have a better understanding of working with useContext. Let us go through useReducer then we combine both and see how they can be merged together.

useReducer

Accepts a reducer of type (state, action) => newState, and returns the current state paired with a dispatch method.. Just take it this way useReducer has similar function as useState. I always say its an upgrade of useState when you need to access different states for example during authentication a user goes through the following. If you have a basic understanding how Redux, Vuex or any other state management works then understanding useReducer should not be difficult.

The are so many states we want to check for the user during there lifecycle in the application. Using useReducer can give us a better approach.
Alt Text

Example
In this example we have a reducer that allows us to have three cases of a user lifecyle _signup, _login and _logout

//__App.tsx__
import React from "react"

const initialState = {
    user: {
        name: ''
    },
    isRegistered: false,
    isLogged: false
}

function reducer(state: any , action: any){
   switch(action.type){
      case "_signup":
         return {
           ...state,
           isRegistered: action.payload.isRegistered
         }
      case "_login":
          return {
           ...state,
           user: action.payload.user,
           isLogged: action.payload.isLogged,
           isRegistered: action.payload.isRegistered
         }
      case "_logout":
         return {
            user: {},
            isLogged: false,
            isRegistered: false
         }
      default:
         throw new Error()
   }
}

function App(){
    const [state, dispatch] = React.useReducer(reducer, initialState)

   return(
     <div>
        <div>{state.user.name}</div>
        <div>{state.isRegistered}</div>
        <div>{state.isLogged}</div>
        <button onClick={() => dispatch({ type: '_signup', payload: { isRegistered: true } })}>sign up</button>
        <button onClick={() => dispatch({ type: '_logout' })}>log out</button>
        <button onClick={() => dispatch({ type: '_login', payload: { user: {name: "Kevin Odongo"}, isLogged: true } })}>log in</button>
     </div>
   )

}
Enter fullscreen mode Exit fullscreen mode

Let us go through the last two hooks then we will see how we can combine useReducer and useContext to create a simple state management with hooks.

useMemo and useCallback

This hook can be used to return memorized values. It is ideal to use them when you have some computation and would like to memorize the values being computed. For example if you are building a calculator memorize the results or a bank loan application you will need to memorize the computation of the loan. Remember they are only called during the component mounts and when the dependencies changes. You should rely on them for optimization purposes.

Example

//__App.tsx__
import React from "react"

function App(){
   // we have a dependencies array and function to handle the logic. 
// memo
   const results = useMemo(() => function(a, b), [dependencies]);
}
// callback
const results = useCallback(() => function(a, b), [dependencies]);
}
Enter fullscreen mode Exit fullscreen mode

There are more hooks out there that you can learn but i believe the ones i have gone through should make you comfortable in building your application.

Simple state management using useReducer and useContext

In this final section let us combine useReducer and useContext and see how we can build a simple state management.

I have prepared the following example to show you how we can build a simple state management using useContext and useReducer.

  • In the example you will note that when a user tries to access dashboard or profile they are denied access until they click login.
  • Once logged in they can access the user information from our context in either the dashboard or profile.

How to implement useReducer and useContext codesandbox example

I hope this tutorial was helpful to someone who is learning React and want to understand more about hooks.

Thanks and see you in the next tutorial.

Discussion (0)