aparnajoshi

ReactJS: Hooks 101

Hooks

React hooks, which got introduced in 16.8 versions, are a new way of using state, and others react lifecycle methods in functional components. React hooks were introduced to solve 3 major problems in the earlier versions of react:

  1. Resuing logic: When multiple components need to share logic, using Higher-order components or Render props is the first approach that comes to mind. However, as the app grows, adding more and more higher-order components is not only difficult, but it also leads to wrapper hell.
  2. Gaint components: A class component contains a lot of code to be executed in its life cycle methods, and sometimes a similar set of logic circles around componentDidMount, componentDidUpdate and componentWillUnmount.
  3. Confusing classes: Understanding ES6 classes the right way can be tricky, and also has additional boilerplate code, just to convert a functional component into a class component.

React hooks were introduced to reduce these problems by providing a stateful primitive simpler than a class component.

Implementation - Using the React hooks

useState:

A simpler way of declaring a state, and state handler in React functional components. useState lets you define an initial value for your state, and provides a state variable and handler function.

const App = () => {
    const [name, setName] = useState('Jackson');
    return(
        <input value={name} onChange={(e) => setName(e.target.value)} />
    );
}

The state, need not be a primitive variable, it can also be an object, however, when an object is being updated, care must be taken to destructure and update all the keys.

const App = () => {
    const [{name1, name2}, setName] = useState({'Jack', 'Jill'});
    return(
        <>
            <input value={name1} onChange={(e) => setName(currentState => ({ 
                name1: e.target.value,
                name2: currentState.name2
            })} />
            <input value={name1} onChange={(e) => setName(currentState => ({ 
                name1: currentState.name1,
                name2: e.target.value
            })} />
        </>
    );
}

useEffect:

useEffect function gets called every time a render takes place. This can also be executed conditionally upon any changes to a specific state variable created by the hooks (via dependency array). Note that, the dependency array passed is only looked for a shallow comparison, and changes to the nested objects will not be considered.

const App = () => {
    useEffect(() => {
        console.log('name variable changed')
    }, [name]);

    const [name, setName] = useState('Jackson');
    const [name2, setName2] = useState('Jill');
    return(
        <>
            <input value={name} onChange={(e) => setName(e.target.value)} />
            <input value={name2} onChange={(e) => setName2(e.target.value)} />
        </>
    );
}

The infamous lifecycle methods can also be replicated with the help of useEffect hook by passing an empty dependency array. Note that when multiple useEffect functions are registered, they are called in the order in which they were registered.

const App = () => {
    useEffect(() => {
        console.log('rendering...');

        return () => {
            console.log('unmounting');
        }
    }, []);

    const [name, setName] = useState('Jackson');
    return(
        <input value={name} onChange={(e) => setName(e.target.value)} />
    );
}

Any API calls or eventHandlers can be set or unset using this pattern.

useRef:

useRef is the compliment of createRef from the class components. Just declare the ref, and use it to get a reference to a DOM entity.

const App = () => {
    const [name, setName] = useState('Jackson');
    const inputRef = useRef();
    return(
        <>
            <input ref={inputRef} value={name} onChange={(e) => setName(e.target.value)} />
            <button onClick={() => inputRef.current.focus()}>focus</button>
        </>
    );
}

The main advantage of useRef is that these variables are not tied to component rendering, and can be used as normal class variables. This can also be often used to check whether a state update is happening while the component is unmounting. This could happen when an API call is slow to return, but the user already moved away from the component.

const App = () => {
    const [name, setName] = useState('Jackson');
    const isMounted = useRef(true);

    useEffect(() => {
        return () => {
            isMounted.current = false;
        }
    }, [])
    return(
        <input ref={inputRef} value={name} onChange={(e) => {
            isMounted.current && setName(e.target. value);
        } />
    );
}

useLayoutEffect:

This hook works similarly to the useEffect hook, however, it is triggered whenever a DOM nodes properties change.

const App = () => {
    const [name, setName] = useState('Jackson');
    const [rect, setRect] = useState({});
    const inputRef = useRef();

    useLayoutEffect(() => {
        setRect(inputRef.current.getBoundingClientRect());
    }, [])
    return(
        <>
            <pre>{JSON.stringify(rect, null, 2)}</pre>
            <input ref={inputRef} value={name} onChange={(e) => setName(e.target.value)} />
        </>
    );
}

useCallback:

This hook is used to prevent re-rendering a child component when the parent reloads(if the child component has dependant functions). Especially when using React.memo. If an array is being iterated over, instead of adding function logic to be handled during the iteration, the method can be passed down to the child component and it can implement the caller logic. This way, multiple method creation can be prevented.

const App = () => {
    const [count, setCount] = useState(0);

    const increment = useCallback(() => {
        setCount(c => c+1);
    }, [setCount])
    return(
        <>
            <Hello increment={increment} />
            <div>count: {count}</div>
        </>
    );
}

const Hello = React.memo({ increment }) => {
    useEffect(() => {
        console.log('renders');
    });

    return <button onClick={increment}>increment</button>
}

The component Hello is rerendered only once, after that, even if the App component is rerendered, Hello remains the same.

useMemo:

Essentially used when the computations must be optimized. Let's say you have a function that recomputes the same value on every render, this process would be tedious and slow. Such recomputations can be avoided with the help of useMemo.

const computeSomethingTedious = (arr) => {
    arr.forEach(nextDigit => {
        if(Math.square(nextDigit) > 45) {
            finalDigit = nextDigit + Math.random(0, nextDigit);
        }
    })
}

const App = () => {
    const finalDigit = 0;
    const [count, setCount] = useState(0);
    const longestWord = useMemo(() => computeSomethingTedious(data), [finalDigit, computeSomethingTedious])
    return(
        <>
            <div>count: {count}</div>
            <button onClick={() => setCount(c => c+1)}>increment</button>
            <div>Computed value: {finalDigit} </div>
        </>
    );
}

The function will not be computed every time the component renders when count updates.

useReducer:

This is an alternative to useState hook and behaves quite similarly to how redux reducers work. The method returns a dispatch function and a value. The dispatch function can be used for updating any value changes that are required.

const reducer = (state, action) => {
    switch(action.type) {
        case 'INCREMENT': return state + 1;
        default: return state
    }
}

const App = () => {
    const [state, dispatch] = useReducer(reducer, 0)
    return(
        <>
            <div>count: {count}</div>
            <button onClick={() => dispatch({ type: 'INCREMENT' })}>increment</button>
        </>
    );
}

useContext:

If the data must be shared throughout the application without necessarily sharing data throughout all the children.

import { createContext } from 'react';
export const UserContext = createContext(null);

// App component
import { UserContext } from '../userContext';
const App = () => {
    return(
        <UserContext.Provider value="Hey, this is context">
            <Home />
            <About />
        </UserContext.Provider>
    );
}

// Home component
const Home = () => {
    const msg = useContext(UserContext);
    return(
        <div>Home Message: {msg}</div>
    );
}


// About component
const About = () => {
    const msg = useContext(UserContext);
    return(
        <div>About Message: {msg}</div>
    );
}

Custom Hooks:

Apart from using the predefined hooks provided by react, we can also create our custom hooks as per the application requirements.

export const useForm = (initialValues) => {
    const [values, setValues] = useState(initialValues);

    return [values, e => {
        setValues({ 
            ...values,
            [e.target.name]: e.target.value
        })
    }]
}

In the above example, useForm can be used to create a bunch of form variables like email, password, name etc. These values can all be handled by just one custom hook instead of writing multple functions and updating them multiple times.

Caveats

While react hooks are pretty amazing, they do have some limitations:

  1. Conditional statements or loops cannot be used inside hooks.
  2. Not all packages can be used directly with the components implementing hooks. A custom hook must be developed for the sake of usage.
  3. Testing with hooks can be tricky.

Aparna Joshi

Written by Aparna Joshi who works as a software engineer in Bangalore. Aparna is also a technology enthusiast, writer, and artist. She has an immense passion and curiosity towards psychology and its implications on human behavior. Her links: Blog, Twitter, Email, Newsletter