All React Hooks and Concepts in One Article

First of all, React is a JS library, not a full-fledged framework, so in order to create a full-fledged web application, you need to know a lot of additional things in order to use them with React. This article will talk about React concepts, hooks and, of course 😊, good practices.

We use React to create reusable components that can be logically used to create UI. Creating components in React is as easy as creating functions 🤩 .

For example 👇🏻, below is a simple react component where we send data as an argument that can be easily specified inside the function.

function Component(props){
	return <h1>{props.text}</h1>
}

Okay, let’s start with what is state in React?

The state is the place where the data intended for the component is stored. When the state changes, the component starts to re-render, which allows us to manage the changed data in the application.

Now let’s learn about state when using useState().


useState()

const component = () => {
	// Давайте вызовем console.log, передадим в него useState 
	// и посмотрим, что он вернет
	console.log(useState(100));
	// Он возвращает массив [100, функция]
	// Возвращается состояние, а также функция для обновления состояния
	// Мы можем деструктуризировать массив 
	// для получения значения состояния и функции
	const [state, setState] = useState(100);


	return (
		<div>
			Привет!!!
		</div>
	)
}

Tip: Only use state inside components.

But you can’t update the state value using the “=” operator, because although this will change the value, it will not start the redrawing of the component. React wants you to pass a value through the setState function to update the state.

Passing a function inside useState()

You can pass a function to useState, in which case the initial value will be equal to what the given function returns, this is useful when you use as the initial value a task that requires significant calculations.

const [state, setState] = useState(() => {
	console.log("initial state");
	return 100;
});

Passing a function inside setState()

You can use a function when you need to update the state based on the data that is currently in the state.

onClick={() => {
	// Аргумент value это текущее состояние 
	setState((value) => {
		return value + 1;
	});
}} 

useEffect()

The useEffect hook has 2 parts, the first is the function and the second is the dependency array, which is optional.

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

We will receive a console log each time the state changes.
For example, if you have 2 states inside a component, and one of them has changed, then we will immediately get a console.log, which is something we usually don’t want.

useEffect(() => {
   console.log('change');
})

Note: The first call to useEffect() will be right after your component is mounted in the DOM.

Array of dependencies

We can specify the state inside the dependency array of useEffect so that it only tracks state changes that were specified inside the array.

const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);
useEffect(() => {
	console.log('state1 changed');
}, [state1])

Reminder: don’t update the state inside useEffect without some thoughtful logic, otherwise it can cause an endless circle of redraws.

Cleaning function

Inside useEffect , you can always return a cleanup function that is used to remove unwanted behavior. The cleanup function is called not only before the component is unmounted, but also before the next effect is executed, read for details (in English).

useEffect(() => {
	console.log(`state1 changed | ${state1}`);
	return () => {
		console.log('state1 unmounted | ', state1);
	}
}, [state1])

You can receive data from api as in the example 👇🏻.

useEffect(() => {
	const url = "https://jsonplaceholder.typicode.com/todos/1";
	const fetchData = () => {
		fetch(url)
			.then(res => res.json())
			.then(data => {
				setState(data.title)
			})
	}
		fetchData();
	}, []);

useContext()

The api context allows you to pass data to child components without specifying them in props.

import { createContext } from "react";
import { useState } from "react";

const StoreContext = createContext();

const component = () => {
    const data = useState({
        name: 'Ritesh',
        email: 'someMail@gmail.com',
    })[0];

    const Child = () => {
        return <div>
            <StoreContext.Consumer>
                {value => <h1>Ваше имя: {value.name}</h1>}
            </StoreContext.Consumer>
        </div>
    }

    return (
        <StoreContext.Provider value={data}>
            <Child />
        </StoreContext.Provider>
    )
}

export default component;

You can wrap the parent component in a Context.Provider and use it inside the Context.Consumer function. What does useContext do? It replaces Context.Consumer and allows us to get data using useContext.

Look at the example 👇🏻.

import { createContext, useContext } from "react";
import { useState } from "react";

const StoreContext = createContext();

const component = () => {
    const data = useState({
        name: 'Ritesh',
        email: 'someMail@gmail.com',
    })[0];

    const Child = () => {
        const value = useContext(StoreContext);
        return <div>
            <h1>Ваше имя: {value.name}</h1>
        </div>
    }

    return (
        <StoreContext.Provider value={data}>
            <Child />
        </StoreContext.Provider>
    )
}

export default component;

More.


useReducer()

useReducer is used to manage state in React, it’s kind of like the reduce function in javascript.

// функция редюсера принимает в себя 2 параметра
// текущее состояние и экшен, и возвращает новое состояние
reducer(currentState, action)
// useReducer принимает в себя также 2 параметра
// функцию редюсера и изначальное состояние
useReducer(reducer, initialState);

Let’s create a simple counter using useReducer

import { useReducer } from 'react'

const initialState = 0;
const reducer = (state, action) => {
    switch (action) {
        case 'increment':
            return state + 1;
        case 'decrement':
            return state - 1;
        default:
            return state;
    }
}

export default function main() {
    const [count, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Значение: {count}</p>
            <button onClick={() => dispatch('increment')}>+</button>
            <button onClick={() => dispatch('decrement')}>-</button>
        </div>
    )
}

We can also make things more difficult by turning our state into an object.

import { useReducer } from 'react'

const initialState = {
    firstCounter: 0,
    secondCounter: 0
};
const reducer = (state, action) => {
    switch (action.type) {
        case 'increment':
            return { ...state, firstCounter: state.firstCounter + action.value };
        case 'decrement':
            return { ...state, firstCounter: state.firstCounter - action.value };
        default:
            return { ...state };
    }
}

export default function main() {
    const [count, dispatch] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Значение: {count.firstCounter}</p>
            <button className="bg-gray-200 p-2" onClick={() => dispatch({ type: 'increment', value: 2 })}>
                Увеличить на 2
            </button>
            <button className="bg-gray-200 p-2" onClick={() => dispatch({ type: 'decrement', value: 4 })}>
                Увеличить на 4
            </button>
        </div>
    )
}

Or, we can use multiple useReducer.

import { useReducer } from 'react'

const initialState = 0;
const reducer = (state, action) => {
    switch (action) {
        case 'increment':
            return state + 1;
        case 'decrement':
            return state - 1;
        default:
            return state;
    }
}

export default function main() {
    const [count, dispatch] = useReducer(reducer, initialState);
    const [count2, dispatch2] = useReducer(reducer, initialState);

    return (
        <div>
            <p>Счетчик: {count}</p>
            <button className="bg-gray-100 p-2 m-2"
                onClick={() => dispatch('decrement')}>-</button>
            <button className="bg-gray-100 p-2 m-2"
                onClick={() => dispatch('increment')}>+</button>

            <p>Счетчик 2: {count2}</p>
            <button className="bg-gray-100 p-2 m-2"
                onClick={() => dispatch2('increment')}>+</button>
            <button className="bg-gray-100 p-2 m-2"
                onClick={() => dispatch2('decrement')}>-</button>
        </div>
    )
}

When to use useState and when to useReducer???

useReducer is preferred over useState when you have complex logic that involves multiple values, or when the state being updated depends on the previous one. useReducer also allows you to optimize the component, since you can pass dispatch from outside instead of a callback.


useReducer() along with useContext()

By using useContext and useReducer together we can manage the global state at any level in the component tree, let’s take a look at an example 👇🏻.

// main.jsx
import React from 'react'
import { useReducer } from 'react'
import ChildrenA from '../components/ChildrenA';

export const StateContext = React.createContext();
const initialState = 0;
const reducer = (state, action) => {
    switch (action) {
        case 'increment':
            return state + 1;
        case 'decrement':
            return state - 1;
        default:
            return state;
    }
}

export default function main() {
    const [count, dispatch] = useReducer(reducer, initialState);
    return (
        <div>
            <StateContext.Provider
                value={{ countState: count, countDispatch: dispatch }}>
                <ChildrenA />
            </StateContext.Provider>
        </div >
    )
}

// ChildrenA.jsx

import React from 'react'
import ChildrenB from './ChildrenB'
import { StateContext } from '../pages/main'
import { useContext } from 'react'

export default function ChildrenA() {
    const { countState, countDispatch } = useContext(StateContext)
    return (
        <div>
            У child A значение счетчика равно {countState}
            <ChildrenB />
        </div>
    )
}

// ChildrenB.jsx

import React from 'react'
import { StateContext } from '../pages/main'
import { useContext } from 'react'

export default function ChildrenB() {
    const { countState, countDispatch } = useContext(StateContext)
    return (
        <div>
            <p>Счетчик равен {countState}</p>
            <button onClick={() => countDispatch('increment')}>+</button>
            <button onClick={() => countDispatch('decrement')}>-</button>
        </div>
    )
}

Both states will be changed at the same time.


useCallback()

Let’s look at the code and try to understand the behavior of functions in React.

import React from 'react'

export default function main() {

    function Sum() {
        return (a, b) => a + b;
    }
    const func1 = Sum();
    const func2 = Sum();
    console.log(func1 === func2);

    return (
        <div>main</div>
    )
}

When we run the code we will see “false” in the logs.

Now, after looking at the example, let’s try to understand how we can use useCallback.

// main.jsx
import React, { useState } from 'react'
import ChildrenA from '../components/ChildrenA';
import ChildrenB from '../components/ChildrenB';
import ChildrenC from '../components/ChildrenC';

const main = () => {
    const [state1, setState1] = useState(0);
    const [state2, setState2] = useState(0);

    const handleClickA = () => {
        setState1(state1 + 1);
    }

    const handleClickB = () => {
        setState2(state2 + 1);
    }

    return (
        <div className="flex flex-col justify-center items-center">
            <ChildrenA value={state1} handleClick={handleClickA} />
            <ChildrenB value={state2} handleClick={handleClickB} />
            <ChildrenC />
        </div>
    )
}

// React.memo позволяет перерисовывать компонет только тогда,
// когда изменяются props
export default React.memo(main);

// ChildrenA.jsx
import React from 'react'

function ChildrenA({ value, handleClick }) {
    console.log('ChildrenA');
    return (
        <div>ChildrenA  {value}
            <button className="bg-gray-200 p-2 m-2" onClick={handleClick} >Click</button>
        </div>

    )
}

export default React.memo(ChildrenA);

// ChildrenB.jsx
import React from 'react'

function ChildrenB({ value, handleClick }) {
    console.log('ChildrenB');
    return (
        <div>ChildrenB {value}
            <button className="bg-gray-200 p-2 m-2" onClick={handleClick} >Click</button>
        </div>
    )
}

export default React.memo(ChildrenB);

// ChildrenC.jsx

import React from 'react'

function ChildrenC() {
    console.log('ChildrenC');
    return (
        <div>ChildrenC</div>
    )
}

export default React.memo(ChildrenC);

When you look at the console.log in the browser, you will see that all three components are being rendered at once, but when you click on any button, only 2 components are redrawn.

Note: we’re using React.memo() here, which is why ChildrenC doesn’t get rerendered because props do not change. But why is it that when ChildrenA changes, ChildrenB is also redrawn?

The reason is that when the parent component is redrawn, the handleClick function is not the same. I explained this in the paragraph above, describing how React detects changes to props. That’s why ChildrenA and ChildrenB are redrawn at once.

To solve this problem, we will use useCallback.

useCallback returns a memoized callback (a memoized function, a function that only changes when one of the items inside the dependency array changes).
useCallback takes a function and an array of dependencies, just like useEffect.

Let’s change our parent component and look at the logs.

// main.jsx

import React, { useState, useCallback } from 'react'
import ChildrenA from '../components/ChildrenA';
import ChildrenB from '../components/ChildrenB';
import ChildrenC from '../components/ChildrenC';

const main = () => {
    const [state1, setState1] = useState(0);
    const [state2, setState2] = useState(0);


    const handleClickA = useCallback(() => {
        setState1(state1 + 1);
    }, [state1])

    const handleClickB = useCallback(() => {
        setState2(state2 + 1);
    }, [state2])

    return (
        <div className="flex flex-col justify-center items-center">
            <ChildrenA value={state1} handleClick={handleClickA} />
            <ChildrenB value={state2} handleClick={handleClickB} />
            <ChildrenC />
        </div>
    )
}

export default React.memo(main);

Now you can make sure that “everything is in chocolate” 👇🏻.


useMemo()

useCallback returns a memoized function, along with this useMemo returns a memoized value. For example, we need to find the factorial, and recalculate the value only when the value changes, and not every time the component is redrawn, so let’s try using useMemo.

import React, { useState, useMemo } from 'react'

function factorialOf(n) {
    console.log('factorialOf(n) called!');
    return n <= 0 ? 1 : n * factorialOf(n - 1);
}

const main = () => {
    const [number, setNumber] = useState(2)
    const factorial = useMemo(() => factorialOf(number), [number])
    const [count, setCount] = useState(0)

    return (
        <div className="flex flex-col justify-center items-center">
            {factorial}
            <button className="bg-gray-200 p-2 m-2" onClick={() => setNumber(number + 1)}>+</button>
            {count} <button className="bg-gray-200 p-2 m-2" onClick={() => setCount(count + 1)}>+</button>
        </div>
    )
}

export default main;

useRef()

Let’s print useRef to the console and see what it returns.

console.log(useRef(100))
// выведится что-то типа 👉🏻 {current: 100}

useRef returns a mutable ref object in which the value “current” is set by the passed argument during initialization (initialValue). The returned object will persist for the life of the component.

When you compare an ordinary object with itself in useEffect, then after redrawing they do not match, and this triggers the useEffect, this does not happen with useRef, you can see this in the example below 👇🏻.

import { useEffect, useState, useRef } from "react";

const component = () => {
    const obj1 = { hi: 100 };
    const obj2 = useRef({ hi: 100 });
    console.log(obj1 === obj2.current);

    const [state, setState] = useState(() => {
        return 1;
    });

    useEffect(() => {
        console.log('obj1 changed | ', obj1);
    }, [obj1])

    useEffect(() => {
        console.log('obj2 changed | ', obj2.current);
    }, [obj2])


    return (
        <div onClick={() => {
            setState((value) => {
                return value + 1;
            });
        }} className="w-screen h-screen flex justify-center items-center text-4xl font-extralight">
            {state}
        </div>
    )
}

export default component;

🥳🥳🥳🥳 Congratulations!!!
We’ve covered all the important hooks. Thanks for reading!

Similar Posts

Leave a Reply