A needed workflow to efficiently execute your React projects.
Introduction
I have constantly been in situations where I would like to render a certain component or some specific data only when a certain condition is met or switch views on a page without routing the user to a different page. Additionally, many are times when I’d prefer to have a different view of each state of the program. For example, having a view while a component is fetching data from the server and having a view if it fails to retrieve the data. This ability to show whichever view I want based on certain conditions means that I can significantly control the user’s experience. A good example would be how online stores show you a button to purchase what’s on your cart, a view with a loader or spinner on purchase, and a success page if your payment goes through. How would you achieve this?
Based on the above, you probably have an idea of what to do or know exactly what to do because of your coding experience(Yes, you React buff). I’m also guessing that you may have already concluded that a simple ternary operator would achieve this effect and assuming you know your way around React you probably have a better solution to this problem. Regardless of your coding experience with React, I would like to share my workflow in such situations. Your feedback on this is invaluable to me :).
A few prerequisites
This article will best serve React developers or those learning the framework. A basic understanding of Redux, Javascript and JSX is also recommended.
I will be using the following dependencies to make it easier to demo ;
- Styled-components: This allows me to write CSS in the javascript files.
- Semantic-UI-React and Semantic-UI-CSS. Semantic provides a lot of easy-to-use, well-designed components like buttons that are well-styled.
- Redux: This will help with state management while we fetch, update and delete records from the user interface.
- Is_js. This is a micro-check library.
- React-hook-form. React hook form will help in form submission.
- JSON-server. This will provide a mock rest API with all the functionality required for the demo. Please ensure that the JSON server is running when you start fetching, updating or deleting records.
Here is a link to the code :
After cloning the repository, run the following commands in your terminal.
npm run json:server // this will start the json server
npm start or yarn start // this will start the react app
The Switcher component
First things first, all the code in this article will revolve around this simple component. The Switcher expects a value prop that will determine what is rendered.
import React, { Fragment } from 'react'
const Switcher = (props) => {
return <Fragment>{props[props.value]}</Fragment>
}
export default Switcher
I recommend that you have this component in its own folder and have an index.js file to export it.
// this is the index.js file
import Switcher from './Switcher'
export default Switcher
Switcher Examples
I will take you through three basic situations where I have used the Switcher. First, initialize a blank react app and install the dependencies I have mentioned above. Once you are set up, create a page where you will demo. Yes, every view in this article will be rendered on a single page but the Switcher will help in showing only what’s relevant.
1. Switch between a table and grid view
Say you have content in your database and have designed a table view to display it. Furthermore, you’ve also designed a different view that would show the same data but has a different interface for your users. I have designed simple cards in a grid-like interface that simply uses flex to align the cards.
We will add buttons that on click will switch between the views but first make sure that your main page imports these two views and the Switcher.
import React, { useState } from "react";
import styled from "styled-components"; // this will help in styling
import { Button, Icon, Container } from "semantic-ui-react";
// these components only require you to add JSX tags
// for better styling rather than code the css
import TableExample from "./components/Pages/TableExample";
import GridExample from "./components/Pages/GridExample";
import Switcher from "./components/Switcher";
Structure your component as you deem fit and add the Switcher component where the main content would have gone.
<Switcher />
This is the code to my example. I have used styled-components hence the custom JSX tags.
const BodyWrapper = styled(Container)`
&&& {
padding: 1em 2em;
}
`;
const HeaderContainer = styled.div`
padding: 1em 0em 0em 2em;
`;
const Paragraph = styled.p`
font-size: 18px;
`;
const ButtonContainer = styled.div`
display: flex;
justify-content: flex-start;
padding: 1em 0em 1em 2em;
`;
export default function App() {
const [view, setView] = useState("grid");
// this will set the default view
return (
<div>
<HeaderContainer>
<h1>Switcher Examples</h1>
<Paragraph>Click below to switch views </Paragraph>
</HeaderContainer>
<ButtonContainer>
<Button
basic
icon
color={view === "grid" ? "grey" : "blue"}
onClick={() => setView("table")} // switch to table view
>
<Icon name="table" />
</Button>
<Button
basic
icon
color={view === "table" ? "grey" : "blue"}
onClick={() => setView("grid")} // switch to grid view
>
<Icon name="address card outline" />
</Button>
</ButtonContainer>
<BodyWrapper fluid>
<Switcher
value={view}
table={<TableExample />}
grid={<GridExample />}
/>
</BodyWrapper>
</div>
);
}
The above snippet is basically composed of a header section, a button section and the main body section with the Switcher. The props on the buttons are for styling and the on-click function changes the current state of which view should be displayed.
The Switcher currently has three props. The value, table and grid. I mentioned earlier that all you need is a value prop on your Switcher and this determines the rules of what will be displayed by the rest of the props.
Currently, the view is set to view which is a string saved in state. This string matches one of the remaining props. All that’s remaining is pointing the table or grid props in the Switcher to the relevant component. Easy, right! Click on the buttons to change state and in turn change views.
const [view, setView] = useState("grid");
Note: The Switcher does not have to be limited to table and grid views. The value determines the views.
2. Show different buttons depending on process types.
Let’s up the ante. I would like that when a user clicks on the write icon ✍, they have a form pop up that enables them to edit the record. The form will have a ‘save’ button that will update the record on the database. The button should show a spinner once they click ‘save’ and on success, it should show ‘Success’ and on failure, it should show a red error button.
I will show you snippets of what I mean and only focus on the Switcher code. Clone my repository if you need to clarify any questions you may have on this aspect.
Create an update record function in your actions.js file and have the modal access it. The actions file will dispatch accordingly based on the server response. In your reducer, have an update record process object that has a status of IDLE. This status will be changed as you send a request to the server and after you receive a response. Like so;
// #region update record details
case actionTypes.UPDATE_RECORD_DETAILS_REQUESTED:
return {
...state,
updateRecordDetailsProcess: {
status: "PROCESSING",
},
};
case actionTypes.UPDATE_RECORD_DETAILS_SUCCEEDED:
return {
...state,
updateRecordDetailsProcess: {
status: "SUCCESS",
},
content: [...action.payload.content],
};
case actionTypes.UPDATE_RECORD_DETAILS_FAILED:
return {
...state,
updateRecordDetailsProcess: {
status: "ERROR",
error: action.payload.error,
},
};
case actionTypes.UPDATE_RECORD_DETAILS_RESET:
return {
...state,
updateRecordDetailsProcess: { status: "IDLE" },
};
// #endregion
The purpose of this ‘status’ key-value pair is to provide a value to the Switcher. The SUCCESS, ERROR, PROCESSING and IDLE values will mean that we have the ability to add different views for each of these values.
Please ensure that your redux is well set up for updating and deleting a record.
Let’s now have the Switcher component read this status value and render different button views.
<Switcher
value={updateRecordDetailsProcess.status}
IDLE={
<Button type="submit" content="Save" primary size="small" />
}
PROCESSING={
<Button
icon
content="Loading"
primary
loading
basic
size="small"
onClick={(e) => e.preventDefault()}
// to prevent the form from submitting the details
// while the current function is running.
/>
}
SUCCESS={
<Button
content="Success"
positive
size="small"
onClick={(e) => e.preventDefault()}
/>
}
ERROR={
<Button
content="Error"
negative
size="small"
onClick={(e) => e.preventDefault()}
/>
}
/>
This code will go where the Save button is placed in your code. I mentioned earlier that I am using react-hook-form for form submission. The submit button will therefore call a function that calls the function we defined in the actions file.
const onSubmit = (data) => {
updateRecordDetails(content.id, data);
};
The result will have the interface looking like this once the user clicks save.
Below are the success and error views.
I have also set up a Switcher for rendering Placeholder components while the page fetches the records. It’s not as cool as the Netflix or YouTube placeholders but it’s enough for my demo.
I hope I haven’t lost you so far as the next part is important.
Currently, I have a function in my modal container that resets the process after SUCCESS or ERROR and closes the modal on SUCCESS. If you haven’t done this then you may have noticed a bug. The ‘record details’ will update but every other record will have the Save button set to Success. This will of course disappear when you reload the page. The reset function solves the problem but not entirely. Here is the code that’s resetting the process
componentDidUpdate(prevProps) {
if (
prevProps.updateRecordDetailsProcess.status !== "SUCCESS" &&
this.props.updateRecordDetailsProcess.status === "SUCCESS"
) {
setTimeout(() => {
this.props.contentActions.resetUpdateRecordDetails();
this.closeModal();
}, 2500);
}
if (
prevProps.updateRecordDetailsProcess.status !== "ERROR" &&
this.props.updateRecordDetailsProcess.status === "ERROR"
) {
setTimeout(() => {
this.props.contentActions.resetUpdateRecordDetails();
}, 2500);
}
}
I say this because once you set up the delete icon to delete the record on click then you’ll see what I mean. Each of the delete icons on the page will show the views you’ve set all at once. This is of course not what we want.
This problem helped me discover the last example.
3. A Switcher inside another Switcher
Considering that the delete button is shared, we need a way of telling the Switcher to only show the loading and success views on the clicked record and not on all the delete buttons on the page.
Here are some images to better define what I mean. One will show the problem and the other will show how I would like it to be.
How would you achieve the second one? Easy. Add a unique key-value pair to the delete record process object. I used id and set it to null. This id will be updated to the id of the clicked record in your delete function in redux.
This is what I mean,
// action.js file
export const deleteRecord = (id) => {
return (dispatch, getState) => {
// Signal the start of the process
dispatch({
type: actionTypes.DELETE_RECORD_REQUESTED,
payload: {
recordId: id,
},
});
// function to delete from the api
const url = `${base_url}/${id}`;
const request = {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
};
fetch(url, request)
.then((response) => {
const newContent = [...getContent(getState())];
const filtered = newContent.filter((item) => item.id !== id);
if (response.status === 200) {
setTimeout(() => {
dispatch({
type: actionTypes.DELETE_RECORD_SUCCEEDED,
payload: {
content: [...filtered],
recordId: id,
},
});
}, 3000);
} else {
dispatch({
type: actionTypes.DELETE_RECORD_FAILED,
payload: {
error: "An error occurred. Please retry.",
recordId: id,
},
});
}
})
.catch((error) => {
dispatch({
type: actionTypes.DELETE_RECORD_FAILED,
payload: {
error: error.message,
recordId: id,
},
});
});
};
};
// reducers file
const INITTIAL_STATE = {
content: [],
fetchContentProcess: {
status: "IDLE",
},
updateRecordDetailsProcess: {
status: "IDLE",
},
deleteRecordProcess: {
status: "IDLE",
recordId: null,
},
};
...
case actionTypes.DELETE_RECORD_REQUESTED:
return {
...state,
deleteRecordProcess: {
status: "PROCESSING",
recordId: action.payload.recordId,
},
};
case actionTypes.DELETE_RECORD_SUCCEEDED:
return {
...state,
deleteRecordProcess: {
status: "SUCCESS",
recordId: action.payload.recordId,
},
content: [...action.payload.content],
};
case actionTypes.DELETE_RECORD_FAILED:
return {
...state,
deleteRecordProcess: {
status: "ERROR",
error: action.payload.error,
recordId: action.payload.recordId,
},
};
case actionTypes.DELETE_RECORD_RESET:
return {
...state,
deleteRecordProcess: { status: "IDLE", recordId: null },
};
Now that the process also has access to the record id, it’s as simple as having the switcher only show the processing, success and error statuses to the record that has an id that matches the delete record process; record id.
<Switcher
value={deleteRecordProcess.recordId === item.id}
true={
<Switcher
value={deleteRecordProcess.status}
PROCESSING={
<Button basic primary loading icon size="mini">
{" "}
<Icon name="trash" />{" "}
</Button>
}
SUCCESS={
<Button positive basic icon size="mini">
{" "}
<Icon name="check" />{" "}
</Button>
}
ERROR={
<Button
negative
basic
icon
size="mini"
onClick={() => deleteRecord(item.id)}
>
{" "}
<Icon name="redo" />{" "}
</Button>
}
/>
}
false={
<Button
basic
size="mini"
color="blue"
icon
onClick={() => deleteRecord(item.id)}
>
<Icon name="trash" />
</Button>
}
/>
The false prop will cover the IDLE status and as long as the record is not being deleted it will have that view. But once the user clicks on delete then the PROCESSING, SUCCESS or ERROR views will come in place.
Challenge
Add a button that will enable the user to add a record. Use the Switcher component on the button that will save the record. The record should appear on the page after save without reloading the page.
Conclusion
With the help of a single component that is defined once in your entire project, you can create a better user experience. The props supplied to this component are not static and you can have as many views as possible.
So, do you need this component in your React project?