Simplifying Redux Bindings with React Hooks
Jul 16, 2019 • 12 Minute Read
Introduction
In this guide, we’ll explore the concept of React Hooks and how it makes our lives easier when coupled with Redux. Lately, React has shifted its focus towards functional components, moving away from the class-based ones. Right now we are reluctant to go with functional components, knowing that we would need to convert it to a class component if we need to utilize life cycle methods or state management for the component at a later stage. But with the introduction of React Hooks adapting to function-based components is trivial. Also with Redux introducing Hooks API.
This guide would be a continuation of Deciding When to Connect Your Component to the Redux Store. The key objectives of these guides are to:
-
Introduce to React Hooks
-
Introduce to Redux Hooks
-
Explore how Hooks simplify the state management process using the ToDo++ example.
React Hooks
Hooks allow you to reuse stateful logic without changing your component hierarchy. They let you use state, and other React features, without writing a class.
Hooks are Javascript functions. They provide a mechanism for a function component to access state and life cycle methods without relying on a wrapper component or a Higher Order Component. In the bigger picture, this massively helps in keeping components small and clean, and keeping related stateful logic in order. The official documentation provides an excellent overview of Hooks, but we will explore a few key aspects of it.
Hooks are meant to be used with functional components. React has a set of inbuilt Hooks that are useful in managing the stateful logic of the component. These are:
-
useState()
-
useEffect()
useState() enables us to access and maintain the local state of a component. With a class component, we used the state property and setState() function for this purpose. In comparison, Hooks provides a simpler API to provide the same functionality. The code below shows a simple counter implemented using a class component and its transformation to a functional component using Hooks.
import React, { Component } from 'react';
export default class CounterComponent extends Component{
constructor(props){
super(props);
this.state = {
counter: 0,
someDemoObject: {
someProperty: ""
}
}
}
handleButtonClick = (e) => {
this.setState({
counter: this.state.counter + 1
});
}
render(){
return (
<div>
<div>
{this.state.counter}
</div>
<div>
<button onClick={this.handleButtonClick}>UP YOU GO</button>
</div>
</div>
)
}
}
import React, { useState } from 'react';
export default function CounterComponent(){
const [counter, setCounter] = useState(0);
const [someObject, setObject] = useState({
someProperty: ""
});
return (
<div>
<div>
{counter}
</div>
<div>
<button onClick={() => setCounter(counter+1)}>
UP YOU GO
</button>
</div>
</div>
)
}
As you can see, the code with functions is simple and clean. Also, an important thing to note is that you use the argument of the useState function to pass the initial state of the variable. For example, the initial state of counter is 0 and it is passed as an argument to the function. You could use useState as many times as you need to initialize the state.
This is neat, but what happens to the life cycle methods? As React developers, we thrive on the use of componentDidMount(), componentDidUpdate() and componentWillUnmount() methods to manage initial API calls required by a component. For example, if we need to retrieve the initial state of the counter from an API at the component mount, this is a problem. useEffect() resolves this issue. The code below extends our previous CounterComponent to outline the use of life cycle methods and useEffect() in functional components.
import React, { Component } from 'react';
export default class CounterComponent extends Component{
constructor(props){
super(props);
this.state = {
counter: 0,
someDemoObject: {
someProperty: ""
}
}
}
componentDidMount(){
this.setState({
counter: thiss.apiCall()
})
}
apiCall(){
// assume that this function is an API call that
// retrieves the initial counter value from a distant server
return 10;
}
handleButtonClick = (e) => {
this.setState({
counter: this.state.counter + 1
});
}
render(){
return (
<div>
<div>
{this.state.counter}
</div>
<div>
<button onClick={this.handleButtonClick}>UP YOU GO</button>
</div>
</div>
)
}
}
import React, { useState, useEffect } from 'react';
function apiCall(){
// assume that this function is an API call that
// retrieves the initial counter value from a distant server
return 10;
}
export default function CounterComponent(){
const [counter, setCounter] = useState(0);
const [someObject, setObject] = useState({
someProperty: ""
});
useEffect(() => {
setCounter(apiCall());
}, [])
return (
<div>
<div>
{counter}
</div>
<div>
<button onClick={() => setCounter(counter+1)}>
UP YOU GO
</button>
</div>
</div>
)
}
Note: The second argument [] sent to the useEffect() function states that we only need to run the code once at the component load. This depicts the behavior of componentDidMount(). By default, useEffect() will re-render if any state prop is changed.
So now we have a basic knowledge of Hooks. Let's dive into the Redux docs to find out about the new APIs they have introduced with Hooks.
Redux Hooks
As much as I love React, the one thing I really dislike is the massive boilerplate code that is required to get the store, reducers, and actions up and running. While the Redux team has introduced (Redux Start Kit)[https://redux-starter-kit.js.org/] to reduce some of the pain, binding the store to components and using actions remains a hassle.
Redux provides three Hooks to integrate the bindings for functional components. Those are:
- useSelector()
- useDispatch()
- useStore()
useSelector() is similar to useState() (at a very high level, the actual API is way different) and provides access to the Redux store state. Which means we can now remove the bulky mapStateToProps section from the code. useDispatch() on the other hand, handles the dispatching of actions to the reducer. This removes the need for a mapDispatchToProps section from the code. Combined, these two Hooks greatly reduce the code overhead in the components. Let's see these in action with the same Counter example from above. For it, assume that the counter is now stored in a Redux store and necessary initiations are done.
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { increaseCount } from './actions';
class CounterComponent extends Component{
render(){
return (
<div>
<div>
{this.props.counter}
</div>
<div>
<button onClick={this.props.increaseCount}>UP YOU GO</button>
</div>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
counter: state.counter
}
}
const mapDispatchToProps = {
increaseCount
}
export default connect(mapStateToProps, mapDispatchToProps)(CounterComponent);
import React, { useState, useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increaseCount } from './actions';
export default function CounterComponent(){
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
return (
<div>
<div>
{counter}
</div>
<div>
<button onClick={() => dispatch(increaseCount())}>
UP YOU GO
</button>
</div>
</div>
)
}
We will not discuss useStore() in the context of this guide, but I recommend reading up on the Redux Hooks from their official docs.
Now armed with this information, let's find out how we can utilize Hooks in a real-world application. For this purpose, we would be using the same idea as we did in the previous guide. We will discuss how the use of Hooks changes the concepts behind using bindings in complex component hierarchies and apply the knowledge to restructure Todo++.
Back to Todo+
In Todo++, we discussed the idea of maintaining the Redux bindings in a complex component hierarchy. We developed the concept of minimizing the points of Redux bindings to increase code maintainability. But with the introduction of Hooks, this concept was revised. Since the binding process is now seamless, newer thoughts around the matter suggest that components should talk to Redux whenever necessary. That being said, it's the developer's responsibility to prevent abusing it.
In transforming our ToDo++ to use Hooks, we will use the following paths.
Moving Actions to Relevant Components
Since we are not bound by a rule to minimize the Redux bindings, we can now let the components directly dispatch their corresponding actions. For an example, CheckBoxComponent will now dispatch the toggleItem action while the ButtonComponent will dispatch printItem and deleteItem actions.
Integrating Hooks
All our connects will be replaced by the useSelector() and useDispatch() methods. This doesn't mean that every component in the hierarchy will be connecting to the store. For example, it still makes more sense to only connect the TodoListComponent to the store (to retrieve the item list). If we try to connect any component below in the hierarchy, we will end up with unnecessary complications.
With these modifications, ToDo++ is now running fully on Hooks! Check out the Github Repo to see how it changed in the code.
Conclusion
In this guide, we explored the shiny new concepts of Hooks in React. React Hooks were introduced to promote the use of functional components over class components. Hooks provide mechanisms for a functional component to access the local state and the life cycle of a component which earlier were privileges of class components.
With React Hooks, Redux introduced its API to simplify the matters. It provides cleaner access to the store and actions with less boilerplate code. So, we were able to re-think our initial thought process of maintaining the Redux bindings in a complex component hierarchy.
Given all these exciting aspects of Hooks, it is important to note that Hooks are very new. It is not advised to implement Hooks in all your existing code bases, at this time. As the maintainer of Redux has stated here, Hooks come with their pros and cons. So, it is better to be mindful when integrating Hooks to your project.