React.js — Basic Hooks (useState, useEffect, & useContext)
By: William Leiby
Tags: React, Programming, JavaScript, Software Engineering, Software Development

Getting Started

useState

useState(), as defined above, hooks state into a component. It’s very easy to incorporate into functional components:

In the screenshot above, we can take note of a few things. useState() has two constants you should recognize during the invocation of a useState() hook:

The state data structure/type itself (e.g. — state ) which will hold the initial value(s) for that instance of state . You can only pass one argument to useState(), but you can have multiple useState() invocations as you’ll see outlined further in the reading.

) which will hold the initial value(s) for that instance of state . You can only pass one argument to useState(), but you can have multiple useState() invocations as you’ll see outlined further in the reading. A setter function (e.g. — setState ) that is executed during a particular point in the component’s lifecycle to update the value(s) of the state’s data structure/type.

Another thing to take note of in the React.js Docs regarding the array destructuring you see in the example above:

When we declare a state variable with useState , it returns a pair — an array with two items. The first item is the current value, and the second is a function that lets us update it. Using [0] and [1] to access them is a bit confusing because they have a specific meaning. This is why we use array destructuring instead. (React.js Docs)

One thing to take note of is that you can call these two variables, [state, setState] , whatever you want. There are two common naming conventions you’ll come across and see most engineers using:

Encapsulate everything in one JavaScript object and assign all state properties to their respective default values. If you’ve worked with class-based React.js components before, this will be very familiar to you. It’s very common to see these two constants named state and setState , but again, feel free to name them whatever you want. A nickname I’ve given to this approach in managing state, and one that I’ll reference to throughout the rest of this article, is “vertically-scaled component state”. Example:

const [state, setState] = useState({

loading: true,

error: '',

signUpFormFields: {

username: '',

password: ''

},

scoreCount: 0

});

Name the first constant in the destructured array a camel-cased version of whatever part of that component’s state you’ll be keeping track of and updating (e.g. — loading, error, isAuthenticated, signupFormFields, etc. ). Then, name the second constant the same as the first, but with set- prefixed to the naming convention along with proper adjustments in capitalization to follow the camel-casing practice. A nickname I’ve given to this approach in managing state, and one that I’ll reference to throughout the rest of this article, is “horizontally-scaled component state”. Example:

const [loading, setLoading] = useState(true); // boolean

const [error, setError] = useState(''); // string

const [signUpFormFields, setSignUpFormFields] = useState({

username: '',

password: '' ,

}); // object

const [value, setValue] = useState(0); // int

Both do the same exact thing (for the most part), albeit they do have their own individual pros and cons:

When it comes to horizontally-scaling your state, a negative is that you will find yourself having to run many more setter function invocations (e.g. — setState() ) to get your component’s state updated to the desired result. Solutions do exist to mitigate this from happening, such as using the useReducer() hook, but that is outside the scope of this article. If you’re looking for a tutorial to implement this solution, I would highly recommend Harry Wolff’s Why I Love useReducer video on YouTube where he does an incredible job going in-depth into the use of the useReducer() hook. A positive in horizontally-scaling your component’s state is when you are using asynchronous calls, handling promises, or using libraries like fetch or axios to perform CRUD operations on APIs that may directly affect your component’s state. The reason for this has to do with how much data you’ll be retrieving from these services, and rewriting or copying over the entirety of a vertically-scaled state object will be far more costly than if you were to just make a single invocation of useState() mutually exclusive to that part of your application’s logic.

) to get your component’s state updated to the desired result. Solutions do exist to mitigate this from happening, such as using the useReducer() hook, but that is outside the scope of this article. If you’re looking for a tutorial to implement this solution, I would highly recommend Harry Wolff’s Why I Love useReducer video on YouTube where he does an incredible job going in-depth into the use of the useReducer() hook. A positive in horizontally-scaling your component’s state is when you are using asynchronous calls, handling promises, or using libraries like fetch or axios to perform CRUD operations on APIs that may directly affect your component’s state. The reason for this has to do with how much data you’ll be retrieving from these services, and rewriting or copying over the entirety of a vertically-scaled state object will be far more costly than if you were to just make a single invocation of useState() mutually exclusive to that part of your application’s logic. Vertically-scaling your state has the upside of whenever you want to update your JavaScript object, you only have to call a single setter function. Some negatives, though, are that unless you are updating every single property in the JavaScript object, you’re going to have leverage JavaScript’s spread syntax so as to not reset altered state values back to their original declarations as defined in the useState() hook. This problem also exists in horizontally-scaled component state, but the spread operations are typically far less costly as you’re typically only handling very small changes. If this sounds a bit confusing, here’s what you would have to do if I took the vertically-scaled example above and wanted to update loading: false :

const [state, setState] = useState({

loading: true,

error: '',

signUpFormFields: {

username: '',

password: ''

},

scoreCount: 0

});



setState({ error: 'A new error entry!' }); /*

If you wanted 'error' to persist its newly set state value ('A new error entry!'), you will have to use the spread operator the next time you updated your state (as I do below). Something to take note of:



We don't have to use the spread operator in the first setState() invocation update above because there haven't been any previous changes to the initial state values.

*/ setState({ ...state, loading: false }); /*

Your updated state object would now look like this: {

loading: false,

error: 'A new error entry!',

signUpFormFields: {

username: '',

password: ''

},

scoreCount: 0

} */

If you’re looking for a best practice on whether you should horizontally or vertically scale your state, the React.js docs recommend using multiple state invocations in place of a single state (i.e. — use horizontally-scaled component state):

“However, we recommend to split state into multiple state variables based on which values tend to change together.” (React.js Docs).

The reasoning behind this has to do primarily with the memoization of React.js components and the state(s) they handle. This is basically a fancy way of saying you should optimize the speed of costly function calls by caching the initial call once, then only doing a fetch on data that’s been altered from the cache. This approach means you will most likely have to incorporate more complex React.js hooks such as useMemo() and/or useCallback(), which falls outside of the scope of this article. If you’re looking for a source to learn how to leverage these more complex React.js hooks, I would recommend Ben Awad’s useMemo and useCallback tutorials on YouTube.

useEffect

useEffect(), as defined earlier, hooks the primary concepts of componentDidMount() , componentDidUpdate() , and componentWillUnmount() into a component via one function invocation. It’s very easy to incorporate into functional components:

useEffect() Hook

In the screenshot above, we can take note of a few things. There are two arguments that are passed to useEffect():

An anonymous callback function that houses your useEffect logic. This logic is executed based upon how you set up useEffect() to run (we will discuss this further below).

The second is an array that takes in comma-delimited variables called the dependency list. This is how you change the way useEffect() operates.

Important mention: If you don’t pass the second argument in the useEffect() hook (i.e. the dependency list), then the hook will run on every single render — this can be problematic if/when you’re using this hook in conjunction with something like useState() because your component could spiral into a re-rendering loop where; First, the component runs the useEffect() hook on the initial render. Then, some data in useState() is updated via the setter function as described above within the useEffect() hook. After the update, the component re-renders because of the state update and executes the same useEffect() hook again. I discuss how to prevent this with how one should use useEffect() below.

Although useEffect() does leverage aspects of componentDidMount() , componentDidUpdate() , and componentWillUnmount() , it’s best not to think of useEffect() as a hook that carbon-copies over the functionality you get from each of these three three lifecycle methods and compresses them into a single function that can be invoked across many instances within your component.

Rather, think of useEffect() as a function that executes specific tasks for your component before rendering (often referred to as “cleaning”), after rendering, and before the component’s unmount.

Since the useEffect() can be used in a plethora of different ways, most of which I will not cover in the scope of this article (no need to worry — I’ll provide resources below that will cover the more edge-case uses of the hook), I will only be covering the ways that you’ll see useEffect() implemented 80% of the time if I were to use Pareto’s principle for this specific React.js feature.

Here are the more common ways useEffect() hooks are implemented:

For a useEffect() invocation to only run on every mount and unmount, use the useEffect() hook in the following manner:

useEffect(() => {

// some component logic to execute...

}, []); /*

Notice the empty array as the second argument above.



We don't pass anything to the array as we don't was useEffect() to depend on anything - thus the purpose of having the dependency list in the first place.

*/

For a useEffect() invocation to run less, or more, often based upon what that useEffect() invocation is dependent on (i.e. — what is passed through to the dependency list), use the useEffect() hook in the following manner:

const [value, setValue] = useState(0); useEffect(() => {

// some component logic to execute...

}, [value, setValue]); /*

Notice the dependency array as the second argument above.



We pass 'value' to the array as an example to showcase how this hook can work. This useEffect() invocation will execute every single time 'value' is updated. Another thing to mention is that arguments in the dependency list don't have to come from other hooks like they do in this example - they can be other forms of data that are assigned to a particular variable where the underlying assigned values can be/are mutated.

*/

Just like useState(), you can use as many instances of useEffect() as your component desires.

Hooks let us split the code based on what it is doing rather than a lifecycle method name. (React.js Docs)

If you need to make bespoke DOM alterations that wouldn’t be covered in the two examples provided above, I would recommend consulting the React.js Docs with a primary focus on the section, “Timing of Effects”.

If you’re looking for an all-encapsulating resource regarding the useEffect() hook that could solve any and all of the problems you have with this particular hook, there is one article I would recommend reading as though it’s React.js gospel: A Complete Guide to useEffect. Written by Dan Abramov, Co-Author of Redux, Create React App, and all-around incredible engineer, the author dives into the nuances and the functionality this particular hook has to offer all providing easy-to-follow examples along the way. I couldn’t recommend a better resource and person to have written about this hook than this exact article.

useContext

Preface — At the time of publishing this article, there is another React.js solution to global state management which is currently in the experimental phase of development. This package is known as Recoil that seems to be a crossover between something like the React.js Context API and an agnostic global state management tool like Redux or MobX, all while bringing new design patterns to the table as to how to manage a front-end’s global state. I will not be discussing this library within the scope of this article, but I felt it was important to mention as the product may progress to a point in the future where it could become the best practice for implementing a global state management solution for React.js front-ends.

Probably one of my favorite additions from the 16.8 update, the React Context API is a suite of API features that provides a mutable, global state data structure to hook into components at any point throughout the component tree, thus preventing a React.js anti-pattern known as prop drilling.

Take the following React.js component tree architecture into account as an example:

React.js Component Tree Example

Let’s say you have some piece of state logic in the index.jsx file located in the Authenticated folder (components/Authenticated) and you want to pass that data down to, for example, the Section3 component (components/Authenticated/Dashboard/Body/Section3). Before the React Context API, you would have to “drill” through each mediating component with props, even if you weren’t going to use that prop data in that said component, before you got to the desirable ancestor component. There are a few reasons why this may not be the most suitable solution for passing data down the component tree, all of which are summed up perfectly in Kent C. Dodds’ blog post on this exact topic:

It [prop drilling] inevitably leads to a very confusing data model for your application. It becomes difficult for anyone to find where data is initialized, where it’s updated, and where it’s used. Answering the question of “can I modify/delete this code without breaking anything?” is difficult to answer in that kind of a world. And that’s the question you should be optimizing for as you code. As an application grows, you may find yourself drilling through many layers of components. It’s not normally a big deal when you write it out initially, but after that code has been worked in for a few weeks, things start to get unwieldy for a few use cases: - Refactor the shape of some data. - Over-forwarding props (passing more props than is necessary) due to (re)moving a component that required some props but they’re no longer needed. - Under-forwarding props + abusing defaultProps so you're not made aware of missing props (also due to (re)moving a component). - Renaming props halfway through making keeping track of that in your brain difficult. (Kent C. Dodds’ Blog)

This is where React’s Context API seeks to mitigate any issues that arise. You can think of React’s Context API as a similar solution to that of well-known global state management tools like Redux or MobX, but with far less overhead and boilerplate along with an approach that feels more native to React.js in place of a tool that operates agnostic to what front-end library/framework you’re using.

Both solutions, of course, have their own pros and cons:

For example, Redux or MobX makes more sense than the React Context API if the state in your application is constantly changing or if you can take on a larger bundle size because you know your product will benefit more from a far more comprehensive suite of global state management. Since a Context.Provider acts basically like one data/structure (i.e. — vertically-scaled state) that can be accessed anywhere in the component tree, updates can also become very costly very quickly if you’re storing lots of properties with continuously updating values within a single invocation of Context.Provider.

On the other hand though, if you’re application is smaller and only requires interaction with the global state very infrequently, or if you’re using multiple invocations of Context.Provider (Not the best solution — you should only use React’s Context API sparingly as stated below) to store different parts of the global state, then it may make more sense to use the Context API over Redux or MobX.

Context is primarily used when some data needs to be accessible by many components at different nesting levels. Apply it sparingly because it makes component reuse more difficult. If you only want to avoid passing some props through many levels, component composition is often a simpler solution than context. (React.js Docs)

Now that we’ve covered the general overview of the React.js Context API, the problem(s) it solves, and its “competition”, here is an example of what a Provider/Consumer relationship looks like when using the Context API along with how to handle updates:

src/providers/SomeProvider/index.jsx

src/index.js

useEffect() Hook from Above + SomeContext Update

The photos above provide one example of how one may use a Context.Provider in parallel with the function createContext() and hook useContext(). If the photos were a bit hard to follow, here is how you can think of it in a potentially more mentally digestible fashion:

A Context object is created via the createContext() function in React.js (line 3 in the first photo). This object always comes with a Provider component (as can be noted by object destructuring in line 5 of the first photo) and Consumer component. The Provider allows Consumers to subscribe to changes to the Context object. The Provider component has a prop called value (line 14 in the first photo) which takes in data that the Provider wishes to pass onto descendant Consumer components. In the example above, we are passing both the state and setState variables from the useState() hook in the Provider’s parent component down to any Consumer components so that two things can happen; First, so Consumer components can access all of the values that initially exist within the Provider’s parent component's state via state . Second, so they have the ability to mutate the state data at any point within the encapsulated component tree (I’m mutating the data in the third photo as can be seen in lines 14–18) via the setState setter function we pass down. The Provider can then be wrapper around any component, at any level, and multiple times within the component tree; In the second photo, you can see that I’m wrapping the Provider component around the entire application. This puts the Provider and the data it can pass down to be consumed at the top of the component tree. If I wanted to nest the Provider component, I could do that as well too. There are theoretically no limits as to where you cannot wrap a Provider component around a Consumer component — You can even wrap multiple Providers around each other as well as allow Consumer components to consume multiple nested contexts (see ‘Consuming Multiple Contexts’ in the React.js Docs for more on this).

One thing you might notice is that I didn’t use the Context.Consumer property for handling consumers. You only have to use Context.Consumer property of the Context object if you want to access the Context object itself, not the Context.Provider. In the example above, we’re accessing the Context.Provider which passes down state and setState via the value prop. This way, we can not just access the state, but also update it in an elegant manner. If you merely wanted to just have your Context object be consumed and not mutated, you could put everything that’s inside the useState() hook directly into the createContext() function in place of null .

If you need more of an understanding with the React.js Context API, I would highly recommend a few video resources: