A minimal React with asynchronous Redux example
In the previous post, we used a simple example to show how application state in React can be managed with Redux.
In this post, we use the example below to show how application state in React can be managed asynchronously with Redux.
What do we mean by asynchronous?
In the previous post’s example, a user selects a course and afterwards we immediately update the Redux store with their choice.
Because we update the Redux store immediately, we can use regular JavaScript running synchronously.
In this post’s example, we update the Redux store with data (UTC time) fetched from an external API, i.e. we have to wait for the API’s response before we can update the Redux store.
For JavaScript in a browser, this waiting means your code runs asynchronously (otherwise the browser would freeze whilst waiting for the network request to complete).
Because the code runs asynchronously, we have to update the Redux store in a different (and more complicated) way to how we did in the previous post.
Design
When the user clicks “Show time”, the UTC time is fetched from an external API every second and the three times are displayed.
When the user clicks “Hide time”, the above polling stops and the times are not displayed.
The UTC time is used to calculate the time in Japan and Barbados.
Components
A natural way to write this UI is to have a root <App />
component with three
child components, <BarbadosTime />
, <JapanTime />
, and <UTC />
.
<BarbadosTime />
, <JapanTime />
, and <UTC />
each display the relevant
time.
<App />
has a button acting as a toggle to show or hide the times.
<UTC />
is responsible for polling the external API to get the UTC time.
State
There are only two values for state to care about:
- Whether to display the times or not
- UTC time
Only <App />
needs to know whether to display the times or not, it is local
state.
<UTC />
needs to know the UTC time because it displays it. <BarbadosTime />
and <JapanTime />
need to know the UTC time because the times they display are
calculated from it (by adding or subtracting the correct number of hours).
UTC time is thus application state.
In the next section, we will see an implementation where UTC time in <UTC />
is lifted up to the parent component <App />
and passed down to <UTC />
,
<BarbadosTime />
and <JapanTime />
, i.e. without Redux.
In the section after that, we will see an implementation where UTC time is managed by Redux (Take me straight there).
Both implementations are available on GitLab
(master
and without-redux
branches).
Implementation
Without Redux
File structure
react-redux-async-example/
├── index.html
└── src
├── components
│ ├── App.jsx
│ ├── BarbadosTime.jsx
│ ├── JapanTime.jsx
│ └── UTC.jsx
└── index.jsx
Boilerplate
index.html
<!DOCTYPE html>
<html>
<head>
<title>React Redux async example</title>
</head>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script
crossorigin
src="https://unpkg.com/react@17/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
></script>
<script
type="text/babel"
src="./src/components/BarbadosTime.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/JapanTime.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/UTC.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/App.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/index.jsx"
data-plugins="transform-modules-umd"
></script>
</html>
Load React, Babel, and our modules.
Container <div>
for the React app.
src/index.jsx
import { App } from "./components/App";
ReactDOM.render(<App />, document.getElementById("root"));
Mount the root component to the correct part of the DOM.
Components
src/components/App.jsx
import { BarbadosTime } from "./BarbadosTime";
import { JapanTime } from "./JapanTime";
import { UTC } from "./UTC";
export const App = () => {
const [showTime, setShowTime] = React.useState(false);
const [utcDateTime, setUtcDateTime] = React.useState(null);
const handleTick = async () => {
const url = "http://worldclockapi.com/api/json/utc/now";
const response = await fetch(url);
const responseData = await response.json();
setUtcDateTime(
new Date(responseData.currentFileTime / 10000 - 11644473600000)
);
};
const Welcome = () => <h1>Welcome</h1>;
if (showTime) {
return (
<>
<Welcome />
<button onClick={() => setShowTime(false)}>Hide time</button>
<UTC onTick={handleTick} utcDateTime={utcDateTime} />
<JapanTime utcDateTime={utcDateTime} />
<BarbadosTime utcDateTime={utcDateTime} />
</>
);
}
return (
<>
<Welcome />
<button onClick={() => setShowTime(true)}>Show time</button>
</>
);
};
UTC time state utcDateTime
lifted up to the root component via handleTick
callback.
UTC time state passed down to <UTC />
, <BarbadosTime />
, and <JapanTime />
components.
src/components/UTC.jsx
export const UTC = ({ onTick, utcDateTime }) => {
let timerId;
React.useEffect(() => {
timerId = setInterval(onTick, 1000);
return () => {
clearInterval(timerId);
};
}, []);
let utcTime = utcDateTime?.toLocaleTimeString();
utcTime = utcTime ?? "...";
return <h3>UTC time is {utcTime}</h3>;
};
When the component is mounted, set the timer so onTick
is called every second.
When the component is unmounted, clear the timer.
src/components/BarbadosTime.jsx
export const BarbadosTime = ({ utcDateTime }) => {
const diff = -4;
const country = "Barbados";
const barbadosDateTime = new Date(utcDateTime?.getTime());
barbadosDateTime?.setHours(barbadosDateTime?.getHours() + diff);
const barbadosTime = barbadosDateTime?.toLocaleTimeString();
return (
<h3>
The time in {country} is{" "}
{barbadosTime === "Invalid Date" ? "..." : barbadosTime}
</h3>
);
};
src/components/JapanTime.jsx
export const JapanTime = ({ utcDateTime }) => {
const diff = 9;
const country = "Japan";
const japanDateTime = new Date(utcDateTime?.getTime());
japanDateTime?.setHours(japanDateTime?.getHours() + diff);
const japanTime = japanDateTime?.toLocaleTimeString();
return (
<h3>
The time in {country} is{" "}
{japanTime === "Invalid Date" ? "..." : japanTime}
</h3>
);
};
With Redux
File structure
react-redux-async-example/
├── index.html
└── src
├── components
│ ├── App.jsx
│ ├── BarbadosTime.jsx
│ ├── JapanTime.jsx
│ └── UTC.jsx
├── index.jsx
└── slice.js
Boilerplate
index.html
<!DOCTYPE html>
<html>
<head>
<title>React Redux async example</title>
</head>
<body>
<div id="root"></div>
</body>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.7.0/react-with-addons.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/3.5.2/redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/4.4.5/react-redux.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux-thunk/2.3.0/redux-thunk.min.js"></script>
<script
crossorigin
src="https://unpkg.com/react@17/umd/react.development.js"
></script>
<script
crossorigin
src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"
></script>
<script
type="text/babel"
src="./src/slice.js"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/BarbadosTime.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/JapanTime.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/UTC.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/components/App.jsx"
data-plugins="transform-modules-umd"
></script>
<script
type="text/babel"
src="./src/index.jsx"
data-plugins="transform-modules-umd"
></script>
</html>
Load extra dependencies for Redux.
Because we are updating Redux asynchronously, we need Redux Thunk also.
src/index.jsx
import { App } from "./components/App";
const initialState = { utcDateTime: null };
const appReducer = (state = initialState, action) => {
if (action.type === "utcDateTimeFetched") {
return {
...state,
utcDateTime: action.payload,
};
}
return state;
};
const store = Redux.createStore(
appReducer,
Redux.applyMiddleware(ReduxThunk.default)
);
ReactDOM.render(
<ReactRedux.Provider store={store}>
<App />
</ReactRedux.Provider>,
document.getElementById("root")
);
Same as for the synchronous case before except now we configure the store with Redux Thunk middleware.
Slice
src/slice.js
const actions = {
fetchUtcDateTime: (payload) => {
return { type: "utcDateTimeFetched", payload };
},
};
export const fetchUtcDateTime = () => {
return async (dispatch) => {
const url = "http://worldclockapi.com/api/json/utc/now";
const response = await fetch(url);
const responseData = await response.json();
const utcDateTime = new Date(
responseData.currentFileTime / 10000 - 11644473600000
);
dispatch(actions.fetchUtcDateTime(utcDateTime));
};
};
Before, we had an action creator selectCourse
in src/actions.js
. This has
been replaced by a thunk action creator fetchUtcDateTime
which returns a thunk
function.
src/slice.js
replaces src/actions.js
.
A thunk function takes at most two arguments (dispatch, getState)
and
typically dispatches an action.
Here, the action returned by actions.fetchUtcDateTime
is dispatched.
Components
src/components/App.jsx
import BarbadosTime from "./BarbadosTime";
import JapanTime from "./JapanTime";
import UTC from "./UTC";
export const App = () => {
const [showTime, setShowTime] = React.useState(false);
const Welcome = () => <h1>Welcome</h1>;
if (showTime) {
return (
<>
<Welcome />
<button onClick={() => setShowTime(false)}>Hide time</button>
<UTC />
<JapanTime />
<BarbadosTime />
</>
);
}
return (
<>
<Welcome />
<button onClick={() => setShowTime(true)}>Show time</button>
</>
);
};
As before, all the application state is elsewhere.
src/components/UTC.jsx
import { fetchUtcDateTime } from "../slice";
const UTC = ({ onTick, utcDateTime }) => {
let timerId;
React.useEffect(() => {
timerId = setInterval(onTick, 1000);
return () => {
clearInterval(timerId);
};
}, []);
let utcTime = utcDateTime?.toLocaleTimeString();
utcTime = utcTime ?? "...";
return <h3>UTC time is {utcTime}</h3>;
};
const mapDispatchToProps = (dispatch) => {
return {
onTick: () => dispatch(fetchUtcDateTime()),
};
};
const mapStateToProps = (state) => {
return {
utcDateTime: state.utcDateTime,
};
};
export default ReactRedux.connect(mapStateToProps, mapDispatchToProps)(UTC);
The main point of interest is
dispatch(fetchUtcDateTime());
Before, in mapDispatchToProps
we dispatched a plain action (an object) created
by the selectCourse
action creator.
Now, we dispatch a thunk function, i.e. a function that dispatches a plain action created by an action creator.
In other words, we have moved the original dispatch into a thunk, and dispatch the thunk.
The thunk is created by the thunk action creator fetchUtcDatetime
.
We could dispatch the thunk directly, i.e. without using a thunk action creator. However, using the latter means:
- We follow the same pattern as before (we used an action creator rather than dispatching the action directly)
- We can make extra arguments available to the thunk and thus pass data in the component to it
src/components/BarbadosTime.jsx
const BarbadosTime = ({ utcDateTime }) => {
const diff = -4;
const country = "Barbados";
const barbadosDateTime = new Date(utcDateTime?.getTime());
barbadosDateTime?.setHours(barbadosDateTime?.getHours() + diff);
const barbadosTime = barbadosDateTime?.toLocaleTimeString();
return (
<h3>
The time in {country} is{" "}
{barbadosTime === "Invalid Date" ? "..." : barbadosTime}
</h3>
);
};
const mapStateToProps = (state) => {
return {
utcDateTime: state.utcDateTime,
};
};
export default ReactRedux.connect(mapStateToProps)(BarbadosTime);
src/components/JapanTime.jsx
const JapanTime = ({ utcDateTime }) => {
const diff = 9;
const country = "Japan";
const japanDateTime = new Date(utcDateTime?.getTime());
japanDateTime?.setHours(japanDateTime?.getHours() + diff);
const japanTime = japanDateTime?.toLocaleTimeString();
return (
<h3>
The time in {country} is{" "}
{japanTime === "Invalid Date" ? "..." : japanTime}
</h3>
);
};
const mapStateToProps = (state) => {
return {
utcDateTime: state.utcDateTime,
};
};
export default ReactRedux.connect(mapStateToProps)(JapanTime);