Custom hooks, Use Context, and States in React

Custom hooks, Use Context, and States in React

Let's get into it

One time, I ran into a problem in my React project and was working on it for a while. I thought my code looked perfect; I rechecked it again and again but was still surprised why my state was changing in Component A but wasn't changing in Component B.

Let me give you a quick summary on what i was trying to do.

I was building a feature that required my state to be updated in both components whenever the state changed in any of the components. In simpler terms, it means that when I make any change in Component A, it should also be updated in Component B and vice versa

I had initially used a custom hook to distribute data to the two components. In the custom hook, I wrote a piece of code to retrieve data from my database, populated my useState with it, and then returned the populated state, distributing it to the two components

Note that the code below demonstrates how my codebase looked when I was stuck. Mine was much larger, but this is a simplified code that would make it easier to explain. I created a React custom hook called useSharedState. It manages a state, sharedState, using useState and returns an object with the current state and a function to update it

// CustomHook.tsx
import { useState, Dispatch, SetStateAction } from 'react';

type SharedStateType = {
    sharedState: number;
    setSharedState: Dispatch<SetStateAction<number>>
}

const useSharedState = (): SharedStateType => {
    const [sharedState, setSharedState] = useState(0);

    return { sharedState, setSharedState };
};

export default useSharedState;

In Component A, I wrote code so that when a button is clicked, changes would be made to the state. Since both components were using the same state from a custom hook, I expected the changes made in Component A to reflect in Component B. However, I was wrong. The state was only updated in Component A and not in Component B. I felt surprised, or rather, confused — 'Why isn't this working?' I thought to myself. I stared at my screen for a while, looking at each line of code, trying to figure out the problem in what I believed to be a 'perfect codebase.' It was then that I realized it wasn't as perfect as I had thought.

// ComponentA.tsx
import React from 'react';
import useSharedState from '../src/hook';

const ComponentA = () => {
    const { sharedState, setSharedState } = useSharedState();

    const handleClick = () => {
        // code to update the state
        setSharedState((prevSharedState: number) => prevSharedState + 1);
    };

    return (
        <div>
            <h2>Component A</h2>
            <p>Shared State: {sharedState}</p>
            <button onClick={handleClick}>Update State</button>
        </div>
    );
};

export default ComponentA;

// ComponentB.tsx
import React from 'react';
import useSharedState from '../src/hook';

const ComponentB = () => {
    const { sharedState } = useSharedState();

    return (
        <div>
            <h2>Component B</h2>
            <p>Shared State: {sharedState}</p>
        </div>
    );
};

export default ComponentB;

Custom hooks and state independence

After a while of surfing the Internet to understand why my code wasn't working, I came across a solution on Stack Overflow. I discovered that custom hooks encapsulate their state and logic, and each component using the custom hook is given its instance. Therefore, changes made to the state in Component A wouldn't automatically reflect in Component B. I shook my head, acknowledging that my code wasn't as flawless as I initially thought.

However, i wasn't done yet. still needed a way to make sure the state changes in both components were in sync. That's when I decided to bring in an old friend — the useContext.

Using useContext for global state

Since React hooks promote state independence, I decided to opt for useContext. useContext enables the management of global states, allowing components to subscribe to a context and receive updates when the context value changes. This ensures that all components using that context reflect the latest state. So, I simply set up a useContext to hold the shared state and provide it to the components.

import React, { createContext, useContext, useState, Dispatch, SetStateAction, ReactNode } from 'react';

// sharedState type
type SharedStateType = {
    sharedState: number;
    setSharedState: Dispatch<SetStateAction<number>>;
};

const SharedStateContext = createContext<SharedStateType | null>(null);

// provider component to wrap your app
export const SharedStateProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
    const [sharedState, setSharedState] = useState(0);

    const value: SharedStateType = {
        sharedState,
        setSharedState,
    };

    return (
        <SharedStateContext.Provider value={value}>
            {children}
        </SharedStateContext.Provider>
    );
};

// Custom hook to access the shared state
const useSharedState = (): SharedStateType => {
    const context = useContext(SharedStateContext);
    if (!context) {
        throw new Error('useSharedState must be used within a SharedStateProvider');
    }
    return context;
};

export default useSharedState;
// ComponentA.tsx
import React from 'react';
import useSharedState from '../src/context';

const ComponentA = () => {
    const { sharedState, setSharedState } = useSharedState();

    const handleClick = () => {
        // Update the shared state
        setSharedState((prevSharedState: number) => prevSharedState + 1);
    };

    return (
        <div>
            <h2>Component A</h2>
            <p>Shared State: {sharedState}</p>
            <button onClick={handleClick}>Update State</button>
        </div>
    );
};

export default ComponentA;


// ComponentB.tsx
import React from 'react';
import useSharedState from '../src/context';

const ComponentB = () => {
    const { sharedState } = useSharedState();

    return (
        <div>
            <h2>Component B</h2>
            <p>Shared State: {sharedState}</p>
        </div>
    );
};

export default ComponentB;

Dont forget to wrap your app comp

This way, any change i made in component A reflected in component B and vice versa. I shook my head again, thinking to myself, "when would i write my perfect code", but at least i fixed the issue and learnt that custom hooks have state independence, allowing each component to manage its own local state in isolation while useContext provides a means to share state across components by creating a shared context. I went on to complete three more features, this time, without the idea of writing a "perfect code".