Introduction
The February release of React v16.8 gave the React community a new feature Hooks.
Hooks don't directly add functionality that didn't exist before, instead they introduce a new way of thinking to React JS. Instead of creating function-based components and then making them class-based when state is needed we can now create functional components and add hooks when we need them. Functional components can now get most of the same functionality as class-based components.
This may seem like another feature to add to your projects, but the React team has thought of this and made Hooks a completely opt-in feature. The suggestion from React is to wait until best-practices and patterns are established before adding Hooks into existing projects, and to only add Hooks to simpler new components in the beginning.
For this post I have created an example to describe some of the concepts I am discussing. The code can be found on my GitHub profile.
What are Hooks?
Hooks are functions from the React library which allow function-based components to hook into React and effect underlying processes. The two most important hooks are useState
and useEffect
.
The State Hook
This hook is a function that returns a state variable and a function that can be used for changing the state variable. React’s recommended naming convention is {name_of_state}
and set{name_of_state}
. The function useState
takes one argument which initializes the state with whatever argument is passed.
import React, {useState} from 'react';
function ComponentWithHooks() {
const [count, setCount] = useState(0);
return (
<>
<p>{count}</p>
<button onClick={() => setCount(count + 1)}>Add One</button>
<button onClick={() => setCount(count + 2)}>Add Two</button>
</>
)
};
In this simple example the count state which has been initialized to 0
will be increased by one whenever the first button is pressed and by two whenever the second button is pressed.
An important note is that the state does not need to be an object as in class-based components. Because useState
uses array destructing
to assign variable names useState
can be used multiple times to create several state variables. Using the set{name_of_state}
function will replace the entire state instead of merging it like this.setState
. When using useState
with objects and arrays this is important to keep in mind as it's easy to think of useState
in the same way class-based components use this.setState
.
The Effect Hook
useEffect
is useful for performing side effects. This function is the hook implementation for the lifecycle methods (specifically componentDidMount
, componentDidUpdate
, and componentWillUnmount
). The React documentation suggests that this hook can be used for "network request, manual DOM mutation, logging, and subscribing to external data sources". Of course, this list is limited and potential uses for hooks are nearly limitless. The website useHooks provides several examples of custom hooks which heavily rely on useEffect
.
For this project I didn't focus on useEffect
for a few reasons
-
I wanted to focus on the base functionality that hooks give.
-
useEffect
is designed for creating more complicated hooks. As Dan Abramov notes in this Twitter thread
The Others
The other hooks also provide powerful possibilities especially when used in tandem. One which I have found particularly useful is by using a combination of useReducer
and useContext
it is possible to avoid passings callback functions through every level of the tree; a pattern shown in the Hooks FAQ
There are several other hooks with React has provided in 16.8 and there will likely be several more added in the future. Besides the pattern I mentioned above I will leave it up to the API Reference section of the React docs to describe them.
The addition of hooks to React has sparked some conversation about whether it's better to start using hooks or if the class-based functions we have been using are still better in terms of performance. There is a lot of nuance in the differences between these two and a lot of discussion about the differences Dan Abramov has addressed some of these differences in a blog post.
Overall the difference between the two does not seem to be huge or worth spending too much time on. Both patterns have subtle differences that need to be accounted for and will have different advantages and disadvantages.
An Example
To take a look at how hooks will work and look in a project I have built out an example of a simple shopping platform which uses some of the patterns found in React's docs. The UI will consist of a few common elements which fulfill the following requirements:
-
Show each of the items that are available for purchase
-
Show how many items are in the cart
-
Show which items are in the cart after clicking a button
-
Provide buttons to add and remove items from the cart
Of note the useState
, useContext
, and useReducer
hooks will be used to provide most of the functionality of the app.
The Files
src
| App.jsx
| Cart
| | reducers.js
| | store.js
| components
| | CartCount.js
| | CartTotal.js
| | Items.js
| containers
| | CartPreviewContainer.js
| | ItemsContainer.js
| index.js
Store
The cart uses the useContext
and useReducer
pattern I mentioned above. src/Cart/store.js
is my implementation of the context which gives access to the reducer in order to create a Redux like store which allows any component in the tree to read the state and call dispatch
with useContext
.
const [cartState, cartDispatch] = useReducer(cartItemsReducer, initialState);
return (
<CartContext.Provider value={[cartState, cartDispatch]}>
{props.children}
</CartContext.Provider>
);
One of the main inspirations for creating a store with the context and useReducer
hook came from Vijay Thirugnanam on the post Mimic Redux using Context API and useReducer Hook .
In my case I wrapped my entire app in this context but there is a lot of flexibility with this pattern and it would make sense to create several different context which contain state relevant to different parts of an application.
<Store initialValue={initialCart}>
<CartPreviewContainer />
<ItemsContainer />
</Store>
Subscribing to the store is simple:
const [cartState, cartDispatch] = useContext(CartContext);
How is useReducer
different from Redux?
This Stack Overflow answer succinctly answers this question. The biggest takeaway for me is that we cannot currently add middleware via useReducer
. This limitation makes it difficult to use useReducer
for large applications, it's probably possible to create most of the Redux functionality via hooks, but why recreate what is already well established?
For a small application there are several advantages for useReducer
. The biggest of them is the simple implementation setting up a context which gives components access to reducers is simple, and gives most of the functionality of Redux with only a few lines of code. Further this functionality is built in; there's no extra packages to install.
Using the Store
Each of my container components use useContext
to gain access to both reading the state and dispatching actions. Dispatching an action is as simple as calling the function with whatever parameters are required by the reducer. In my example I use the convention set by Redux, but this could easily be changed.
cartDispatch({type: 'remove', itemId: itemId});
Some Advantages
Hooks have some pretty significant advantages over class-based components.
Simplified API
Hooks provide a simplified API that does not require a lot of setup in most cases. In class-based components there are quite a few rules to consider about where setState
can be called, this.state
can be assigned. These are not too difficult after awhile, but breaking these rules can cause problems that won't be found without linting rules to find them. For example useState
requires to be defined and initialized with a value:
const [count, setCount] = useState(0);
After initialing the value changing the state requires little thought compared to setState
which requires consideration about which lifecycles it is being called in.
useEffect
and useLayoutEffect
also simplify components when compared to the class-based lifecycles. These functions allow users to make one choice about when they need an effect to occur. Either after (useEffect
) or before (useLayoutEffect
) the browser updates the screen.
The effect hooks also allow for conditional firing based on whether certain values have changed. These values are passed as an array in second argument of the function:
useEffect(
() => {
document.title = `${count} items in cart.`;
},
[count],
);
in this case useEffect
will only fire when count
changes. This allows for the function to skip unnecessary operations. Because we can include multiple effect hooks in any component we can easily fine-tune what operations get run when variables change.
Sharing Logic
Redux came in to fix the problem of sharing logic and state across several components and it works very well. It is nice to now have that ability in React without any external library. Hooks allow for reuse that can help clean up some of the consequences of patterns like render props and HOCs. The advantage hooks have over these other patterns is that they don't require changing the structure of the components.
No Classes
In the past classes were the only way to add state and other complex React features to a component. Using classes for components could lead to some confusion about this
and passing callbacks. Things that felt as if they should just work could lead to odd consequences that are difficult to debug and work around. Being able to add state and side effects to components without upgrading it to a class-based component is pretty freeing.
In Summary
React Hooks provide the common functionality that's provided with class-based components in a way that's less confusing and more modular than class-based components, they provide functionality that is normally part of an outside library, and most importantly they allow for customization and reuse. As more people start using hooks there we be more more custom hooks being built and shared in npm and a greater understanding of the way that various patterns work and effect our projects.