In previous lessons and projects, you have learned to build React components using Redux. Now it’s time to explore ways to modify your approach using hooks.
When you complete this lesson, you will be able to
useSelector
and useDispatch
hooksIn order to use hooks in Redux, your application will need to utilize the react-redux
package. If you need a refresher on what this kind of application looks like, see the Starting Point section at the end of this reading or clone the intro-to-redux-hooks repository from GitHub and look at the starter folder.
Consider a simple application that displays the user’s current IP Address with a button to start the lookup. You many even include a loading message which shows while the server call is running.
// ./src/App.js
import React, { useEffect, useState } from 'react';
const App = props => {
const [ip, setIP] = useState(null);
const [loading, setLoading] = useState(false);
const getMyIP = () => {
setIP('(coming soon)');
};
useEffect(() => {
setLoading(ip === "");
}, [ip]);
return (
<div>
<h1>Get My IP</h1>
{loading
? <p>Loading...</p>
: <p>{ip}</p>
}
<button
onClick={getMyIP}
disabled={loading}
>{ip ? 'Again' : 'Go'}</button>
</div>
);
};
export default App;
Notice that this framework uses your knowledge of the useState
hook to simulate the server call and the useEffect
hook to cause the loading indicator to show at the appropriate times.
Now you can update this example to use Redux hooks to replace the fake loading of ip
.
useSelector
Begin by importing useSelector
from the React Redux package.
Assuming you have a reducer with the property ipAddress
, then you can use the useSelector
hook to access the ipAddress
from your Redux store’s state.
In the sample App.js above, using the useSelector
hook would replace const [ip, setIP] = useState('')
. Your component would receive the ip
via your Redux store’s state.ipAddress
, instead of the component’s ip
state.
As a reminder, the
useState
hook in this example is simply mimicking a fetch response. Upon clicking the button with thegetMyIP
click handler, a fetch call is mimicked with thesetIP('(coming soon)')
method. You will need to remove this line as well. Don’t worry you’ll replace it momentarily using another Redux hook.
You can access any available property this way and even call useSelector()
multiple times within a single function component. You can even use props or route parameters to determine what to extract from the store.
Here is an example using props. Assume you have a store with a users
object in its state. Furthermore, you want to get just user
based on the id
provided in a prop to a function component.
Here is the component’s code. See if you can spot where the “magic” happens.
import React from "react";
import { useSelector } from "react-redux";
const UserCard = props => {
const user = useSelector(state => state.users[props.id]);
return <div>{todo.text}</div>;
};
export default UserCard;
If you said the magic happens in the function passed to the useSelector
hook, then you would be correct. Specifically the square bracket notation is used to get just a part of the users
object. Remember, you’re passing a function as the argument to useSelector
; therefore you can use all your skills to determine the right object or value to return
.
useDispatch
In order to trigger an action in Redux, you will need to utilize a different hook; specifically, useDispatch()
. This hook returns a function which you can call to dispatch the action.
Exactly how you use dispatch depends on your Redux setup. There are some minor differences based on whether you decided to use redux-thunk
in your project. The configuration of Redux is beyond the scope of this reading and is something you saw in previous activities. Two solutions are provided in the sample, so you can make the choice which works best for your project. Here’s a quick look at these two options.
In this configuration, you will need to dispatch actions created in your Redux component (e.g. src/store/ipAddress.js). For example, one possible action creator function might look like export const setIP = ip => ({ type: SET_IP, ip });
.
Any functions which perform loading operations will need to be asynchronous and return the value or object retrieved; perhaps in a scenario like this…
// relevant snippet from of src/store/ipAddress.js
export const loadIP = async () => {
// ...
// do stuff here like a fetch with await
// ...
// return the result
return origin
};
Back in the component with the UI (e.g. src/App.js), you’ll need to start by importing these functions as well as adding useDispatch
to the import for Redux.
import { useDispatch, useSelector } from "react-redux";
import { loadIP, setIP } from "./store/ipAddress";
Then use these with your button click handler. Notice you dispatch is using the action to set the value of the ip variable that you just got with useSelector
.
// relevant snippet from src/App.js
const dispatch = useDispatch();
const getMyIP = async () => {
dispatch(setIP(""));
const origin = await loadIP();
dispatch(setIP(origin));
};
The example dispatches two values for the IP Address. The first dispatch call sets the IP address to an empty string (so that the old value no longer shows in the UI while the newer value is loading). The second, of course, is the result of the fetch (or any other kind of service call, of course).
One advantage of this approach is that you will not need to install redux-thunk
or add it to the Redux configuration. However, this comes with the trade-off that actions will be dispatched throughout the application, including in UI components.
Now consider the difference using redux-thunk
. The action function remains unchanged (export const setIP = ip => ({ type: SET_IP, ip });
). The loadIP
function will do its own dispatching (this means a double function in the declaration that results in the code below).
// relevant snippet from ./src/App.js
export const loadIP = () => async dispatch => {
dispatch(setIP(""));
// ...
// do stuff here like a fetch with await
// ...
// dispatch the result
dispatch(setIP(origin));
};
In the component (e.g. _src/App.js), you’ll need to import only the loadIP()
function (and not the setIP
action creator function) (while still importing useDispatch
, of course).
Then the click handler for the button simplifies to
The advantages of this approach using Redux Thunk is the separation of responsibilities where the load and action dispatches are all together resulting in simplified handling within the UI components.
The trade-off is double functions in your Redux (like export const loadIP = () => async dispatch => {
) and the one-time install and setup of redux-thunk
.
Ultimately the decision on the approach is made by each development team based on their personal preference.
In order to refactor an existing class component from the classic approach to using hooks, there are several steps that need to be taken:
useState
hookuseEffect
hook for side-effect management, instead of the componentDidMount
and componentDidUpdate
methodsuseSelector
hooks to replace the mapStateToProps
functionuseDispatch
hook to use dispatch
and replace the mapDispatchToProps
functionexport
to just the component name by removing connect
The best way to understand exactly what to do is to see an example. This will be provided in an upcoming video lesson.
The react-redux
package comes with several hooks which can be used to replace mapStateToProps
, mapDispatchToProps
and connect
. Hooks are used with function components, so remember to start with one if you intend to use hooks; otherwise you’ll need to convert your class component to a function component.
The useSelector
hook give you access to any and all props that are exposed through the state in a Redux store by passing in a function to resolve the state property you want (e.g. useSelector(state => state.theProp)
). The useDispatch
hook allows you to trigger an action directly or by calling a function that uses redux-thunk
to dispatch the action.
Using hooks with React Redux can improve the readability and maintainability of a React project.
For future reference, there are a few additional (advanced and rarely used) features in the official documentation on hooks in React Redux.
As promised, here is an example of setting up the framework with Redux for the “Get My IP” application discussed throughout this reading. This version includes Redux Thunk.
You may access the starter project, the solution project with Redux Thunk, and the solution project without Redux Thunk by cloning the intro-to-redux-hooks repository.
Start with create-react-app
and install react-redux
, redux-thunk
and their dependencies (e.g. redux
) as you’ve done previously.
Wrap your application in the Redux Provider …
// ./src/index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import configureStore from './store/configureStore';
const store = configureStore();
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
… and configure a Redux store …
// ./src/store/configureStore.js
import { createStore, applyMiddleware, combineReducers, compose } from 'redux';
import thunk from 'redux-thunk';
import ipAddress from './ipAddress';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const reducer = combineReducers({
ipAddress,
});
const configureStore = initialState => {
return createStore(
reducer,
initialState,
composeEnhancers(applyMiddleware(thunk)),
);
};
export default configureStore;
… which includes a reducer, an action creator function, and a thunk action creator function …
// ./src/store/ipAddress.js
import { ipUrl } from '../config';
const SET_IP = 'ipAddress/SET_IP';
export const setIP = ip => ({ type: SET_IP, ip });
export const loadIP = () => async dispatch => {
dispatch(setIP(""));
const response = await fetch(`${ipUrl}/ip`, {
method: 'get',
headers: { 'Content-Type': 'application/json' },
});
if (response.ok) {
let { origin } = await response.json();
// obscure last segment for privacy purposes
origin = origin.split('.', 3).join('.') + ".xxx";
// dispatch the result
dispatch(setIP(origin));
}
};
export default function reducer(state = {}, action) {
switch (action.type) {
case SET_IP: {
return {
...state,
ip: action.ip,
};
}
default: return state;
}
}
… that relies on the application configuration …
… to fetch the IP Address using the ip query at httpbin.org.