Introduction: In my previous blog post, I introduced React hooks, explained how to use them and why, and showed how I refactored my Beach Journal project with them. I used React’s useState
and React Redux’s useDispatch
hooks to refactor a few of my components. If you haven’t read that blog post yet, I would highly suggest doing so now, as it will get more complicated from here. In this blog post, Part 2, I will demonstrate how I used useDispatch
, useEffect
, and useSelector
to refactor my App component.
Recap: useDispatch
In my last blog post, I introduced the useDispatch
hook and demonstrated how I used it to refactor a couple of components. Just to review, useDispatch
essentially replaces React Redux’s connect
and mapDispatchToProps
methods. Here’s an example of how to use it, based off of the official documentation.
Without the useDispatch
hook, your component might look like this:
import React, { Component } from 'react'
import { connect } from 'react-redux'
class CounterComponent extends Component {
const { value, incrementCounter } = this.props
render() {
return (
<div>
<span>{value}</span>
<button onClick={() => incrementCounter()}>
Increment counter
</button>
</div>
)
}
}
const mapDispatchToProps = dispatch => {
return {
incrementCounter: () => dispatch({ type: 'increment-counter' })
}
}
export default connect(null, mapDispatchToProps)(CounterComponent)
With the useDispatch
hook, your component becomes much simpler:
import React from 'react'
import { useDispatch } from 'react-redux'
export const CounterComponent = ({ value }) => {
const dispatch = useDispatch()
return (
<div>
<span>{value}</span>
<button onClick={() => dispatch({ type: 'increment-counter' })}>
Increment counter
</button>
</div>
)
}
The useEffect
hook
useEffect
is a nifty little React hook that saves you a lot of hassle with component class lifecycle methods. The useEffect
hook deals with any side effects in your components. According to the React hooks documentation, “you can think of [the] useEffect
hook as componentDidMount
, componentDidUpdate
, and componentWillUnmount
combined”. However, according to that documentation, there is an important difference:
Unlike
componentDidMount
orcomponentDidUpdate
, effects scheduled withuseEffect
don’t block the browser from updating the screen. This makes your app feel more responsive. The majority of effects don’t need to happen synchronously. In the uncommon cases where they do (such as measuring the layout), there is a separateuseLayoutEffect
Hook with an API identical touseEffect
.
I won’t go into the useLayoutEffect
hook here, as I did not use it to refactor my project. But feel free to check out that link if you’re curious!
Conceptually, side effects can be separated into two categories: ones that don’t require cleanup, and ones that do. Let’s check out this example of an effect that doesn’t require cleanup. With class components, if you wanted to update your document’s title every time you clicked a button, you might write something like this (again, all credit goes to the React developers for this example):
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
};
}
componentDidMount() {
document.title = `You clicked ${this.state.count} times`;
}
componentDidUpdate() {
document.title = `You clicked ${this.state.count} times`;
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
When the document first loads, the title in your browser tab will say, “You clicked 0 times”; the same message will also be displayed in the document’s body. When you click the button, the title and body will say, “You clicked 1 times”, then “You clicked 2 times”, etc.
Note that the code in the componentDidMount
and componentDidUpdate
lifecycle methods, is the same. Since we just want the same effect to happen after every render, ideally it should only be written in one method and called in one place. This is impossible with class components. However, it is possible with a functional component and the useEffect
hook! Check this out (again, from the React hooks documentation):
import React, { useState, useEffect } from 'react';
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = `You clicked ${count} times`;
});
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
Way simpler-looking, isn’t it? What’s happening here is that we’re passing our intended effect (updating our document title to display a counter) as a function to useEffect
. That effect is run after the component’s initial render and after every update; this effectively combines componentDidMount
and componentDidUpdate
into one easy-to-use hook!
One major advantage here is that useEffect
puts all of our code for an effect in one place, rather than splitting it up into different component lifecycle methods. Another advantage, as shown by the React documentation, is that useEffect
can be called multiple times, each time with a different effect. What this means is that you can now keep code related to one effect in one place and keep it separate from code related to other effects!
The React documentation also explains how to use the useEffect
hook on effects that require cleanup (i.e. effects that require componentWillUnmount
in class components). For the sake of brevity - and because my project used an effect that did not require cleanup - I won’t go into detail about it here. But to summarize it, you just have useEffect
return an optional function that cleans up the effect.
Refactoring the App component with useEffect
and useDispatch
With all of that said, my App component needed to use useEffect
in a more advanced way. When the component loaded, I wanted to execute the side effect of fetching all of the app’s beach and journal entry data from the backend. However, I only wanted to do this once, after the initial render. So, how do you do that with useEffect
?
It turns out that you can specify an array of dependencies as a second, optional argument to useEffect
! The component will now only update when the value of at least one of those dependencies changes. In my case, the dependency was my dispatch
variable.
With that in mind, my App component went (in part) from looking like this:
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchBeaches } from "./actions/beachActions";
// Other import statements
class App extends Component {
componentDidMount() {
this.props.fetchBeaches()
}
render() {
// Other code
return (
{/* Page content */}
);
}
}
const mapDispatchToProps = dispatch => ({
fetchBeaches: () => dispatch( fetchBeaches() )
});
export default connect(null, mapDispatchToProps)(App);
To looking like this:
import React, { useEffect } from 'react';
import { useDispatch } from 'react-redux';
import { fetchBeaches } from "./actions/beachActions";
// Other import statements
const App = () => {
// Other code
const dispatch = useDispatch();
useEffect(
() => dispatch(fetchBeaches()),
[dispatch]
);
return (
{/* Page content */}
);
}
export default App;
I could technically omit dispatch
as a dependency, since it’s highly unlikely to change. However, because I set dispatch
to the return value of the useDispatch
hook from React Redux - a separate library that React doesn’t know about - I got this warning:
React Hook useEffect has a missing dependency: ‘dispatch’. Either include it or remove the dependency array react-hooks/exhaustive-deps
There are also rare occasions where the value of dispatch
could change, such as passing a new store to the <Provider>
component. But more often than not, your code is fine without it. I’m of the opinion that it’s better to include dispatch
in the dependency array, just to be on the safe side and get rid of that warning. For more information, check out this handy Stack Overflow article.
The useSelector
hook
The useSelector
hook is another handy feature provided by React Redux. Whereas useDispatch
replaces mapDispatchToProps
, useSelector
virtually replaces mapStateToProps
. I say “virtually” because although useSelector
does a lot of the same things as connect
and mapStateToProps
, there are some important differences according to the official documentation. Perhaps most noticeably, useSelector
can return any type of value, whereas mapStateToProps
always returns a JavaScript object. In my next blog post, I will go into more detail about how useSelector
differs from connect
and mapStateToProps
.
In my case, I wanted to display a loading message in my App component while the Beach Journal was retrieving its data from the backend.
This is how I implemented it with a class component:
import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux';
import Navbar from "./components/Navbar";
// Other import statements
class App extends Component {
// Other code
render() {
let pageContent;
if (this.props.retrievingData) {
pageContent =
<>
<h1>Welcome to the Beach Journal!</h1>
<p>Please wait while we load your saved beaches...</p>
</>;
} else {
pageContent =
<>
<Navbar />
<section>
{/* Other page content */}
</section>
</>
}
return (
<div className="App">
{/* Other JSX code */}
{pageContent}
</div>
);
}
}
const mapStateToProps = state => ({
retrievingData: state.beachData.retrievingData
});
export default connect(mapStateToProps)(App);
This is how the App component looked after I refactored it with useSelector
:
import React from 'react';
import './App.css';
import { useSelector } from 'react-redux';
import Navbar from "./components/Navbar";
// Other import statements
const App = () => {
// Other code
let pageContent;
const retrievingData = useSelector( state => state.beachData.retrievingData );
if (retrievingData) {
pageContent =
<>
<h1>Welcome to the Beach Journal!</h1>
<p>Please wait while we load your saved beaches...</p>
</>;
} else {
pageContent =
<>
<Navbar />
<section>
{/* Other page content */}
</section>
</>
}
return (
<div className="App">
{/* Other JSX code */}
{pageContent}
</div>
);
}
export default App;
Just like with useEffect
, the useSelector
hook accepts a selector function as an argument. According to the React Redux documentation, that function is “called with the entire Redux store state as its only argument” and returns the part of the state that you specify in the selector. As I mentioned earlier, that return value can be anything, whereas mapStateToProps
always returns an object.
In my App component above, mapStateToProps
would return something like:
{
retrievingData: true // Or false
}
and (together with the connect
method) pass that object as a prop to the App component.
But useSelector
would just return true
or false
, which can then be saved to a variable within the App component itself. Way simpler - it uses less code and eliminates the need to pass in Redux state as props!
This is admittedly a very simple use case of useSelector
. In the next blog post, I’ll provide a more advanced example of how I used useSelector
with the Reselect library and memoization in my BeachesContainer component.
Putting it all together
When I put all of these hooks together, the App component went from looking like this:
import React, { Component } from 'react';
import './App.css';
import { connect } from 'react-redux';
import { fetchBeaches } from "./actions/beachActions";
import Navbar from "./components/Navbar";
// Other import statements
class App extends Component {
componentDidMount() {
this.props.fetchBeaches()
}
render() {
let pageContent;
if (this.props.retrievingData) {
pageContent =
<>
<h1>Welcome to the Beach Journal!</h1>
<p>Please wait while we load your saved beaches...</p>
</>;
} else {
pageContent =
<>
<Navbar />
{/* Other page content */}
</>
}
return (
<div className="App">
{/* Other JSX */}
{pageContent}
</div>
);
}
}
const mapStateToProps = state => ({
retrievingData: state.beachData.retrievingData
});
const mapDispatchToProps = dispatch => ({
fetchBeaches: () => dispatch( fetchBeaches() )
});
export default connect(mapStateToProps, mapDispatchToProps)(App);
To looking like this:
import React, { useEffect } from 'react';
import './App.css';
import { useSelector, useDispatch } from 'react-redux';
import { fetchBeaches } from "./actions/beachActions";
import Navbar from "./components/Navbar";
// Other import statements
const App = () => {
let pageContent;
const dispatch = useDispatch();
const retrievingData = useSelector( state => state.beachData.retrievingData );
useEffect(
() => dispatch(fetchBeaches()),
[dispatch]
);
if (retrievingData) {
pageContent =
<>
<h1>Welcome to the Beach Journal!</h1>
<p>Please wait while we load your saved beaches...</p>
</>;
} else {
pageContent =
<>
<Navbar />
<section>
{/* Other page content */}
</section>
</>
}
return (
<div className="App">
{/* Other JSX */}
{pageContent}
</div>
);
}
export default App;
Side note: For the sake of brevity, I’ve only included the relevant parts of the App component. If you want to see the entire App component with its changes, check out this Git commit.
This is probably why the official documentation itself recommends against refactoring existing, complicated components with hooks. I made all of these changes in one commit because my code would have broken otherwise! In retrospect, it would have been better just to make a temporary file and use it to refactor my App component one step at a time.
Conclusion
Congratulations, you’ve made it to the end of my long blog post! To summarize, I described how to use React’s useEffect
hook and React Redux’s useDispatch
and useSelector
hooks. I then demonstrated how I used all three hooks to refactor my Beach Journal’s App component.
useEffect
greatly simplifies your React code by combining related code from separate side-effect-related lifecycle methods like componentDidMount
, componentDidUpdate
, and componentWillUnmount
. It also lets you separate code for unrelated side effects. This makes your components much easier to understand and should therefore lead to fewer bugs.
useDispatch
and useSelector
virtually replace React Redux’s connect
, mapDispatchToProps
, and mapStateToProps
methods. (As I said, though, there are some important differences between how useSelector
works, and how connect
and mapStateToProps
work; I will go into more detail in Part 3.) As with useEffect
and other hooks, these greatly simplify your components, making them easier to read and less prone to bugs.
As I mentioned in the Putting it all together section above and in the previous blog post, I don’t (in retrospect) recommend refactoring complicated components with hooks. If you still decide to do this, try to do it one step at a time with a temporary file. If you don’t use a temporary file, you might realize that you’ll have to make a ton of changes to your component(s) at once in order for your app not to break. And in so doing, you may accidentally introduce bugs.
In Part 3, the last blog post of the “Refactoring my React/Redux Project with Hooks” series, I will demonstrate how I used the useSelector
and useLocation
hooks, the Reselect library, and memoization to refactor my BeachesContainer component. Stay tuned, and thanks for reading!
Resources
- Blog post: “Refactoring my React/Redux Project with Hooks, Part 1”
- React Redux’s
useDispatch
hook - React’s
useEffect
hook - A detailed explanation of
useEffect
and how it differs fromcomponentDidMount
andcomponentDidUpdate
- React’s
useLayoutEffect
hook - Side effect example that doesn’t require cleanup (class component)
- Side effect example that doesn’t require cleanup (hooks)
- Using multiple effects to separate concerns
- Side effects that require cleanup
- How to optimize app performance by applying effects only when needed
- Stack Overflow article explaining when React Redux’s
dispatch
function could change - Documentation for React Redux hooks
- React Redux’s
useSelector
hook - Git commit for refactoring the Beach Journal’s App component with hooks
- Gradually adopting hooks vs. refactoring existing components with them