Monday, November 8, 2021

Create Redux App with ASync Ajax Calls




 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
    • return only empty <div/>
  • 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.


No comments:

Post a Comment