DEV Community

willp11
willp11

Posted on • Edited on

React Hooks

Introduction
React Hooks are the modern way of writing React applications. Initially, React components that required the use of any state were always class-based as functional components did not have a way of storing state. However, there were some pain points with using classes that led to hooks being released in React 16.8, which hoped to address these pain points by enabling the use of state and other features within functional components. This tutorial will look at the main features of React hooks, demonstrating the various hooks that are available and some use cases for each.

Managing State
I will demonstrate three hooks that can be used to manage state within functional components; useState, useReducer and useContext.
useState
useState
The useState hook is the simplest of all the hooks. It enables the storage of simple state variables within a component. Whenever you require some simple state to be stored, you should always turn to useState as your first option.
The useState hook returns an array containing the variable that will hold the state and a function that updates that variable. It is common to name the function set[Variable].

const [counter, setCounter] = useState();

return (
    <>
        <p>{counter}</p>
        <button onClick={()=>setCounter(counter+1)}>Increment</button>
    <>
)
Enter fullscreen mode Exit fullscreen mode

You cannot mutate the variable without using the function returned from useState that is designated for that purpose. The example below is not valid React code.

let [counter, setCounter] = useState();
counter += 1;
Enter fullscreen mode Exit fullscreen mode

useReducer
The useReducer hook should be used when you have more complex state, as by using useReducer you can define multiple functions to update the state.
First, you need to define the reducer. The reducer is a function that takes the state and an action as inputs and mutates the state depending on the type of the action.
Here is a simple example of adding and removing items from a shopping cart.

const addItem = (state, item) => {
    return {
        state,
        [item.name]: item
    }
}
const removeItem = (state, item) => {
    let cart = {state};
    delete cart[item.name];
    return cart;
}
const cartReducer = (state, action) => {
    switch (action.type) {
        case ADD_ITEM: 
            return addItem(state, action.item);
        case REMOVE_ITEM:
            return removeItem(state, action.item);
        default:
            return state;
    }
}
Enter fullscreen mode Exit fullscreen mode

Once you have defined the reducer, use the useReducer hook to access and update the state.

const [cart, dispatch] = useReducer(cartReducer);
let item = {name: t-shirt, price: 10};
dispatch({type: ADD_ITEM, item});
Enter fullscreen mode Exit fullscreen mode

useContext
Context allows us to manage global state within a React app without the use of any 3rd party libraries such as Redux.

To use context, you need to create a context object using React.createContext(). The context object has a provider property which in turn takes a “value” prop. Store the state you wish to use globally as the “value” prop.

const CartContext = React.createContext();

const CartProvider = ({children}) => {
    const [cart, dispatch] = useReducer(cartReducer);
    return <CartContext.Provider value={[cart,dispatch]}>{children}</CartContext.Provider>
}
Enter fullscreen mode Exit fullscreen mode

Wrap the provider around any components that need to use the state.

<CartProvider><AnotherComponent /><CartProvider>
Enter fullscreen mode Exit fullscreen mode

Then use the useContext hook to access the state within those components.

const AnotherComponent = () => {
    const [cart, dispatch] = useContext(CartContext);
    
}
Enter fullscreen mode Exit fullscreen mode

useEffect
The useEffect hook is a very important hook as it essentially replaces the component lifecycle methods that were previously used in class-based components. Anything that you previously did with componentDidMount, componentDidUpdate and componentWillUnmount can now be done with the useEffect hook.

Side effects such as mutations, subscriptions, timers and logging must not be done within the main body of a functional component (React’s render phase), as it can cause confusing bugs and inconsistencies within the UI. Instead, useEffect will run after the render phase.

The useEffect hook takes a function and an optional dependency array as inputs. By default, the function defined within useEffect will run after every render (if no dependency array is provided). To adjust when the useEffect hook runs, we adjust the values within the dependency array. An empty dependency array means the effect will run just once when the component mounts.

No dependency array: runs after every render.

useEffect(()=>{
    
});
Enter fullscreen mode Exit fullscreen mode

Empty dependency array: runs once after first render.

useEffect(()=>{
    
}, []);
Enter fullscreen mode Exit fullscreen mode

Has dependencies: runs whenever any of the dependencies changes.

useEffect(()=>{
    
}, [dep1, dep2]);
Enter fullscreen mode Exit fullscreen mode

One of the most common use-cases for useEffect is to retrieve data from an external API. For example, a component with a list of products may need to retrieve all the product data when the component mounts. Note that you cannot define useEffect as async, therefore to use await you must define the async function within useEffect then call it within that same useEffect.

const [products, setProducts] = useState();

useEffect(()=>{
   const getProducts = async () => {
        const res = await fetch(/products);
        const products = res.json();
        setProducts(products);
    }
    getProducts();
}, [])
Enter fullscreen mode Exit fullscreen mode

To avoid memory leaks when using useEffect for a subscription, you may need to unsubscribe when the component unmounts. To do this, use a return statement within the useEffect to perform any clean up that is required.
In the example below, a counter starts when the component mounts and then is cleared after the component unmounts.

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

useEffect(()=>{
    const counter = setInterval(()=>setCount(count=>count+1), 1000);
    return () => clearInterval(counter);
}, [])
Enter fullscreen mode Exit fullscreen mode

In the situation above, you must be careful to use setCount(count=>count+1) and NOT setCount(count+1). This is because if you do not pass a callback function to setCount, it will use a stale reference and be forever stuck on 1. In this case, the console will give a warning that you are missing count as a dependency. If you put count into the dependency array, it will give the appearance that you have solved the problem, however in reality you will be creating a brand new interval every second as useEffect gets called again and again.

Another common use case is to update a component once a user has logged in. For example, a user logs in and receives an authentication token that enables them to retrieve restricted data from an API, such as data about their user profile. In this case, we want to fetch the user profile data once the auth token changes.

const [authState, dispatch] = useContext(AuthContext);
const [user, setUser] = useState();

useEffect(()=>{
    const getUser = async () => {
        const res = await fetch(/getUser, {headers: {Authorization: Token  + authState.token}}
        setUser(res.json());
    }
    getUser();
}, [authState.token])
Enter fullscreen mode Exit fullscreen mode

useRef
The last hook I will cover in this post is useRef. The useRef hook has two main uses; access DOM elements directly and when you want a mutable value that persists for the entire lifecycle of the component but which doesn't cause the component to re-render. To access the value in the ref, you use the ref's current property.

A common example for when you want to access DOM elements directly, is to click on something and then cause another component to gain focus.

    const inputRef = useRef();

    const focusInput = () => {
        inputRef.current.focus();
    }

    return (
        <>
            <input type="text" ref={inputRef}/>
            <button onClick={focusInput}>Focus</button>
        </>
    )
Enter fullscreen mode Exit fullscreen mode

If you ever have a situation where you need to keep a value between re-renders but mutating that value doesn't need to cause a re-render, then consider using useRef instead of useState.

That's it for this article. Next time, I will discuss useRef, useCallback and writing your own custom hooks.

Top comments (4)

Collapse
 
citronbrick profile image
CitronBrick

Please consider formatting your code both inline & code blocks. Code blocks can be prefixed with /**javascript

Collapse
 
brense profile image
Rense Bakker

IMHO it's best to completely forget the old lifecycle methods. useEffect is its own thing and I've seen a lot of unintended behavior, bugs and performance issues in apps that tried to use the useEffect hook to replace their old lifecycle hooks. useEffect is intended for side effects that need to run during rendering. The most common mistake for example is people doing state updates in useEffect, which results in another render, which can result in unexpected app behavior bad performance or even crashes due to infinite loops. 99% of the time what people put in useEffect should really have been a useMemo instead.

Collapse
 
willp11 profile image
willp11

Yes useEffect is it's own thing and not 100% the same as the old lifecycle methods, although the comparison still stands as a lot of things that would have been done in the old lifecycle methods are now done in useEffect.
I don't agree that updating state in useEffect is a bad thing, the re-render that is caused by updating state is often precisely what is wanted, such as if you fetch some data you probably want to display that data and therefore want the UI to re-render. If you don't want it to re-render then use a ref to store that data.
I don't really get what you mean by using useMemo instead of useEffect, could you provide an example of something that people often put into useEffect that should be using useMemo instead? Or do you mean that many people don't memoize a dependency therefore causing useEffect to fire unnecessarily?

Collapse
 
brense profile image
Rense Bakker

I see this happen a lot:

const [myState, setMyState] = useState([])
const [derivedState, setDerivedState] = useState([])
useEffect(() => {
  setDerivedState(myState.filter(item => item.isActive))
}, [myState])
Enter fullscreen mode Exit fullscreen mode

Infact some people use that pattern by default :(