In this post we will create a react/redux app with async ajax calls.
I'm adding this example, because I believe this app should be the base for any react/redux application with ajax calls. This includes:
- Proxy handling for the ajax requests
- Ajax requests wrapper
- Notification top bar to present errors
- An example component to show the world time
Setup and Cleanup
We start from the redux base template:
npx create-react-app my-app --template redux
Delete the counter feature by the following:
- delete folder src/features/counter
- in src/App.js
- in src/app/store.js
- remove import for counter reducer
- remove the counter from the store
- move src/app/store.js to src/store.js
Proxy
Add middleware to allow proxy requests to other services:
npm i -s http-proxy-middleware
And add the proxy configuration:
src/setupProxy.js
const {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://worldclockapi.com/',
changeOrigin: true,
logLevel: 'debug',
}),
)
}
Requests Wrapper
Add a utility to handle sending ajax requests, and parsing the errors.
src/features/request/request.js
export async function sendRequest(method, url, body) {
let fetchOptions = {
method: method,
headers: {
'Content-type': 'application/json',
},
}
if (body) {fetchOptions.body = JSON.stringify(body)}
const response = await fetch(url, fetchOptions)
if (response.status === 200) {
const json = await response.json()
return {
ok: true,
response: json,
}
}
let error = await response.text()
try {
const parsed = JSON.parse(error)
if (parsed.message) {
error = parsed.message
}
} catch (e) {
// ignore - send the actual error text
}
return {
ok: false,
response: error,
}
}
Notification/Errors Top Bar
Add a top bar notification with ability to show info/error notifications
src/features/notifications/component.js
import React from 'react'
import {useSelector} from 'react-redux'
import {selectState} from './slice'
import styles from './component.module.css'
export function Notifications() {
const state = useSelector(selectState)
let key = 0
const items = state.items.map(item =>
(
<div
key={key++}
className={styles.notification}
>
{item.text}
</div>
))
return (
<div>
{items}
</div>
)
}
src/features/component.module.css
.notification {
font-weight: bold;
color: red;
}
src/features/slice.js
import {createSlice} from '@reduxjs/toolkit'
const initialState = {
items: [],
}
export const slice = createSlice({
name: 'notifications',
initialState,
reducers: {
add: (state, action) => {
state.items.push(action.payload)
},
cleanup: (state, action) => {
const expire = Date.now() - 5000
state.items = state.items.filter(item => item.time > expire)
},
},
})
const {add, cleanup} = slice.actions
export const tick = () => (dispatch, getState) => {
dispatch(cleanup())
const state = getState().notifications
if (state.items.length > 0) {
setTimeout(() => {
dispatch(tick())
}, 1000)
}
}
export const addNotification = (isError, text) => (dispatch) => {
const item = {
text,
isError,
time: Date.now(),
}
dispatch(add(item))
dispatch(tick())
}
export const selectState = (state) => state.notifications
export default slice.reducer
World Time Component Example
Create our own feature to display the current time from worldclockapi.com.
src/features/worldtime/component.js
import React from 'react'
import {useDispatch, useSelector} from 'react-redux'
import {errorAjax, load, selectState} from './slice'
import styles from './component.module.css'
export function WorldTime() {
const dispatch = useDispatch()
const state = useSelector(selectState)
if (state.loading) {
return (
<div className={styles.loading}>
Loading
</div>
)
}
return (
<div>
<div className={styles.queries}>
{state.queries} queries
</div>
<div className={styles.time}>
{state.time}
</div>
<div
className={styles.button}
onClick={
() => {
dispatch(load())
}
}>
Reload
</div>
<div
className={styles.button}
onClick={
() => {
dispatch(errorAjax())
}
}>
Make request error
</div>
</div>
)
}
src/features/worldtime/component.module.css
.loading {
position: absolute;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(128, 128, 128, 0.75);
}
.time {
width: 100%;
text-align: center;
}
.queries {
width: 100%;
text-align: center;
}
.button {
background-color: blue;
border-radius: 10px;
cursor: pointer;
width: 200px;
padding: 20px;
margin: 20px;
}
src/features/worldtime/slice.js
import {createAsyncThunk, createSlice} from '@reduxjs/toolkit'
import {sendRequest} from '../request/request'
import {addNotification} from '../notifications/slice'
const initialState = {
time: 'Unknown',
queries: 0,
loading: false,
}
export const load = createAsyncThunk(
'worldtime/load',
async (arg, thunkApi) => {
const {ok, response} = await sendRequest('get', '/api/json/est/now')
if (ok) {
return response
}
thunkApi.dispatch(addNotification(true, response))
},
)
export const errorAjax = createAsyncThunk(
'worldtime/load',
async (arg, thunkApi) => {
const {ok, response} = await sendRequest('post', '/api/not-here')
if (ok) {
return response
}
thunkApi.dispatch(addNotification(true, response))
},
)
export const slice = createSlice({
name: 'worldtime',
initialState,
reducers: {},
extraReducers: (builder) => {
builder
.addCase(load.pending, (state) => {
state.loading = true
})
.addCase(load.fulfilled, (state, action) => {
state.loading = false
if (!action.payload) {
// request had failed, do not update
return
}
state.queries++
state.time = action.payload.currentDateTime
})
},
})
export const selectState = (state) => state.worldtime
export default slice.reducer
Integrate into the Application
Now we call to the reducers from the main reducer store:
src/store.js
import {configureStore} from '@reduxjs/toolkit'
import worldtime from './features/worldtime/slice'
import notifications from './features/notifications/slice'
export const store = configureStore({
reducer: {
worldtime,
notifications,
},
})
and include the components from the main app:
src/App.js
import React from 'react'
import './App.css'
import {WorldTime} from './features/worldtime/component'
import {Notifications} from './features/notifications/component'
function App() {
return (
<div>
<Notifications/>
<WorldTime/>
</div>
)
}
export default App
To make the load time run upon application start, call it from the index
index.js (partial)
import {load} from './features/worldtime/slice'
store.dispatch(load())
ReactDOM.render(
Final Note
As you might have noticed, I've spent ~zero time in making this application beautiful. The goal of this post is to handle the functional aspects of react/redux + ajax calls. I believe that the create-react-app template might had been much better if it included these as part of the basic template.