The Why
Creating engaging user interfaces can be quite difficult. It is very crucial that our apps guarantee great user experiences. In this article, we will look at how to communicate processes to the user using Redux.
It is important that we distinguish our app data into 2 categories – Data & processes. Data is the information that is useful to the user. In short, it is the reason that the user is on our app in the first place. Processes are metadata that help us serve the information in a way that is effective and/or efficient.
We tend to focus on the data aspect of the app most of the time, e.g you click a button and see new posts. However, making good decisions around processes can be equally beneficial.
Let us look at a typical example of a user fetching data from a source, say posts. There are a number of ways that the interactions can go. Just to name a few, they could succeed or fail – but it’s not just limited to failure, It could be that the user entered the wrong information. That is a totally different error than if the user had been disconnected.
To craft a delightful user experience, it is useful to think of the user as going through a number of events. Each event ushers the user into a certain user interface (or change in the same UI). It is after we visualize the user journey that we can model it in our react/redux app.
The implementation
Now that we have an idea of the journey a user goes through, we can now begin to model our redux store.
All the code shared can be found in this Github repository
Setting up
Using create-react-app we can set up a simple react application
npx create-react-app redux-processes
cd redux-processes
npm install --save redux react-redux styled-components redux-thunk
npm start
User Journey
We are now ready to build our redux store. Here is a small illustration of the user journey with accompanying events
Alternatively, things could go wrong and your user journey would look something like this
Folder structure
We shall create a folder called posts to house our redux components. We will also update the App.js to connect to our redux store.
src:
App.js // already exists
App.css // already exists
index.js // already exists
...
posts:
actionTypes.js
actions.js
processTypes.js
PostsPage.js
reducers.js
selectors.js
Events
we use action types to represent events. Redux creates a new state whenever we dispatch an action, our action types merely document the reason for the change. In our current example, we shall have these event types
//actions.js
export const FETCH_POSTS_REQUESTED = "FETCH_POSTS_REQUESTED"
export const FETCH_POSTS_SUCCEEDED = "FETCH_POSTS_SUCCEEDED"
export const FETCH_POSTS_FAILED = "FETCH_POSTS_FAILED"
export const FETCH_POSTS_DISCONNECTED = "FETCH_POSTS_DISCONNECTED"
Processes
Because our processes can be in one of many states at any given time, we should declare a list of all possible states.
//processTypes.js
export const IDLE = "IDLE"
export const PROCESSING = "PROCESSING"
export const error = "ERROR"
export const SUCCESS = "SUCCESS"
export const DISCONNECTED = "DISCONNECTED"
Data
we will declare our initial store state in our reducer as follows.
//reducer.js
import * as processTypes from "./processTypes"
import * as actionTypes from "actionTypes"
const initialState = {
fetchPostsProcess: { status:processTypes.IDLE, message:"" },
posts:[]
}
We have created an array to store our posts and an object to store our process with its current status and message(if any). You will note that our posts are in different objects. This is useful when using a persistence library such as redux-persist and you want to persist the store but reset the processes.
Updating the store
let us update the reducer to translate actions created into a new store.
// reducer.js
import * as processTypes from "./processTypes";
import * as actionTypes from "actionTypes";
const initialState = {
fetchPostsProcess: { status: processTypes.IDLE, message: "" },
posts: [],
};
export default function postsReducer(state = initialState, action = {}) {
switch (action.type) {
case actionTypes.FETCH_POSTS_REQUESTED:
// update the process to show processing
return {
...state,
fetchPostsProcess: { status: processTypes.PROCESSING, message: "" },
};
case actionTypes.FETCH_POSTS_SUCCEEDED:
// update the process to show success and add the data fetched to store
return {
...state,
fetchPostsProcess: { status: processTypes.SUCCESS, message: "" },
posts: action.payload.posts,
};
case actionTypes.FETCH_POSTS_FAILED:
// update the process to show an error with the error message from the action
return {
...state,
fetchPostsProcess: {
status: processTypes.ERROR,
message: action.payload.message,
},
};
case actionTypes.FETCH_POSTS_DISCONNECTED:
// update the process to disconnected
return {
...state,
fetchPostsProcess: {
status: processTypes.DISCONNECTED,
message: action.payload.message,
},
};
case actionTypes.FETCH_POSTS_RESET:
// return our process and data to initial
return {
...state,
fetchPostsProcess: initialState.fetchPostsProcess,
posts: initialState.posts,
};
default:
return state;
}
}
Fetching user data
our action creators will fetch our data from an API and dispatch actions that transition our app to the next state
// actions.js
import * as actionTypes from "./actionTypes";
export const fetchPosts = () => {
return (dispatch) => {
dispatch({
type: actionTypes.FETCH_POSTS_REQUESTED,
});
fetch("<https://jsonplaceholder.typicode.com/posts>", { method: "GET" })
.then((response) => {
if (response.status === 200) {
response.json().then((posts) => {
dispatch({
type: actionTypes.FETCH_POSTS_SUCCEEDED,
payload: { posts },
});
});
} else {
dispatch({
type: actionTypes.FETCH_POSTS_FAILED,
payload: {
message: "An error occurred while fetching posts. please retry",
},
});
}
})
.catch(() => {
dispatch({
type: actionTypes.FETCH_POSTS_DISCONNECTED,
payload: {
message:
"Unable to conenect to internet. Please check your internet",
},
});
});
};
};
Selectors
Selectors expose our redux variables to our UI. It helps to have them separate for when you want to memorize your data for better performance
// selectors.js
export const getFetchPostsProcess = (state) => state.fetchPostsProcess;
export const getPosts = (state) => state.posts;
Components
Let’s now write a simple Page that connects to redux and starts the whole fetching process. We are using a simple Switcher that renders differently based on the current status of the process.
// PostsPage.js
import { connect } from "react-redux";
import React, { Component, Fragment } from "react";
import { bindActionCreators } from "redux";
import PropTypes from "prop-types";
import logo from "../logo.svg";
//import actions and selectors from the Store folder
import * as postActions from "./actions";
import * as postSelectors from "./selectors";
const Switcher = (props) => <Fragment>{props[props.value]}</Fragment>;
Switcher.propTypes = { value: PropTypes.isRequired };
class PostsPage extends Component {
//when the component is mount, call the fetch post action
componentDidMount() {
this.props.postActions.fetchPosts();
}
//get alert from the
showPost(post) {
alert(post.body);
}
render() {
return (
<div>
<h1>Posts</h1>
<Switcher
value={this.props.fetchPostProcess.status}
IDLE={
<div>
<h1>Posts</h1>
</div>
}
PROCESSING={
<img src={logo} className="App-logo" alt="logo" />
}
SUCCESS={
<div>
{this.props.posts.map((post, index) => (
<div key={index}>
<h2>{post.title}</h2>
</div>
))}
</div>
}
ERROR={
<div>
<h1>Error</h1>
<p>{this.props.fetchPostProcess.message}</p>
</div>
}
DISCONNECTED={
<div>
<h1>Unable to connect.</h1>
<p>{this.props.fetchPostProcess.message}</p>
</div>
}
/>
</div>
);
}
}
PostsPage.propTypes = {
fetchPostProcess: PropTypes.object.isRequired,
posts: PropTypes.array.isRequired,
postActions: PropTypes.object.isRequired,
};
//get the posts and the fetch status from the post selector
const mapStateToProps = (state) => {
return {
fetchPostProcess: postSelectors.getFetchPostsProcess(state),
posts: postSelectors.getPosts(state),
};
};
// map all actions in the post store to prop: postActions
const mapDispatchToProps = (dispatch) => {
return {
postActions: bindActionCreators(postActions, dispatch),
};
};
export default connect(mapStateToProps, mapDispatchToProps)(PostsPage);
Wrapping it up
Lastly, let’s update the App.js
to render our file and page and introduce our store. We will also update our App.css
slightly
import React from "react";
import { Provider } from "react-redux";
import { combineReducers, createStore, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import logo from "./logo.svg";
import "./App.css";
//import the post screen
import PostsPage from "./posts/PostsPage";
//import all your reducers from the store
import postsReducer from "./posts/reducers";
//
const rootReducer = combineReducers({
posts: postsReducer,
});
const store = createStore(rootReducer, applyMiddleware(thunk));
const App = () => (
<Provider store={store}>
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Redux processes
</p>
</header>
<PostsPage />
</Provider>
);
export default App;
.App {
text-align: center;
}
App.css
.App-logo {
height: 20vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 5vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
Congratulations, you now have a working redux process boilerplate to build. Go ye forth and build complex UI’s.
Mobile
Here is a small example of how this same approach could be used in a mobile (React Native) app.
Conclusion
I hope this helps makes sense of working with redux in complex scenarios. I’m open to feedback and additions.