⟵back

React hooks, pt. 1

Plot twist: what I'm using the most at work right now is React with TypeScript.

My team is made up of devs with varying levels of full-stack experience, and I would say easily over half of the tickets from our client project in the six months I've been there have been partially or fully frontend-based.

So, needless to say, I've had to up my React game real quick. This has been a steep learning curve, sometimes causing me to really doubt my abilities as a developer. Yet I have to keep reminding myself that I'm dealing with difficult concepts for the first time within a complex, pre-existing context — rather than trying them out in small projects at my own pace. Luckily, people in my team are overall understanding and helpful whenever I run into trouble.

I haven't ever used other JavaScript frontend frameworks and libraries, such as Vue and Angular, so I can't speak to that. However, one thing that stands out to me about React, in comparison to the backend stuff I'm used to working with, is its hooks. These are also known as "lifecycle hooks".

Quick primer on the three phases of the React component lifecycle:

  1. Mounting: the initial render, when the component is inserted into the DOM.
  2. Updating: the component is re-rendered, triggered every time the state or the props change.
  3. Unmounting: the component is removed from the DOM.

In the past, with class-based components, component state would be managed with "lifecyle methods". This is why when looking at older React documentation and tutorials, you will come across stuff like componentDidMount(), where you had to explicitly write what should happen during that particular phase of the lifecycle. However, this is now legacy. Functional components are the React standard and using hooks is now the only way to manage state.

But seeing as I never used class-based components, it's probably an advantage that I got to learn React with functional components from scratch, so there's nothing I need to "translate" from before.

So, here are the most common hooks I've used so far. Before I begin: all these hooks will need to first be imported from React before they can be used in a component.

useState

This is the hook that I came across first, and is arguably the most widespread. It's most useful for introducing state to a component, a key feature of React, meaning you can use it in projects with little complexity.

const [isLoading, setIsLoading] = useState<boolean>(false)

It consists of an array variable with two elements — the value and the setter — and then you define the default state (according to the value type).

One disadvantage of this hook is that each state change re-renders the component, so it should be used sparingly if possible — more on that in the useRef section.

useEffect

It took me a while to grasp the difference between useEffect and useState, but they are actually for fundamentally different purposes. It's important to first understand what an "effect" is in programming; essentially, it is a function. A "side effect", in turn, is defined as any effect occurring within a function that is not its return value.

A common use case for useEffect is the fetching of data. You pass an anonymous function as an argument and define what you want to happen, which results in a state change (as you can see, it's used in conjunction with useState in this example).

const [tags, setTags] = useState<Array<string>>([]);
    
    useEffect(() => {
        (async () => {
            const response = await fetch("https://example.org/api/v1/tags", {
                method: "GET",
            }).then((response) => response.json());
             setTags(response);
        })();
    }, [tags]);

What is [tags] doing at the end there? The square brackets are a dependency array. That value from the state variable will need to be consistently referred to, otherwise the entire effect will keep getting re-executed unnecessarily, rather than just checking that one variable.

useContext

Although I'd briefly tinkered with context in one of my React side projects — then approaching it as an alternative to useState — when I encountered it in my current job, it felt very intimidating.

First you must create a context (ideally in a separate module), then pass it into useContext.

const PlaceTypeContext = createContext({
    chosenPlaceType: '',
    setChosenPlaceType: (chosenPlaceType: string) => {}
})

const { chosenPlaceType, setChosenPlaceType } = useContext(PlaceTypeContext)

In this example, the curly braces in the variable setting mean that an object (the PlaceTypeContext object) is being destructured and that chosenPlaceType and setChosenPlaceType are being selected from it.

Another way you can think of this is as getting PlaceTypeContext.chosenPlaceType and PlaceTypeContext.setChosenPlaceType, but now you can access the values more cleanly.

Context itself can actually get pretty complex. In this scaled-up project at work, an API call was made within the creation of the context, and then when a component wanted to access that data from that fetch, useContext was called in that component, or wrapped around another as a provider. So useContext should be understood not simply as an alternative to useState, but as a way for varying components to be able to access the same values, regardless of the component's place in the tree.

useRef

When React renders your functional component, it is calling a function. Something to keep in mind is that re-rendering is costly and should therefore be avoided if possible.

useRef allows you to persist a value, or rather, a reference, across re-renders. When a ref is set, a mutable value called current is created. For example, if you did const numberRef = useRef<number>(0), then numberRef.current would be 0.

A good use case would be showing a message for a certain amount of time (and yep, here are useState and useEffect popping up again for good measure):

const timeout = useRef<NodeJS.Timeout | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false)
const [showLoadingMessage, setShowLoadingMessage] = useState<boolean>(false)

useEffect(() => {
    if (timeout.current) { 
        clearTimeout(timeout.current); 
    }
    if (isLoading) { 
        setShowLoadingMessage(true); 
    }
    timeout.current = setTimeout(() => {
        setShowLoadingMessage(false);
        if (timeout.current) { 
            clearTimeout(timeout.current); 
        }
    }, 5000);
}, [isLoading]);

If isLoading is truthy, we want to show a loading message (triggered by something unseen here, e.g. user input), but only for five continuous seconds. When the timeout ref expires, the loading message will be gone. When isLoading is triggered again, the process will be repeated, irrespective of how many re-renders the changing states may trigger.