Over the holidays I had some time up my sleeve and decided to give Uncle Bob's Clean Code a thorough read through to see what, if any principles of clean code architecture can be applied to some of the React projects I've been working on.
Not too far into the book, Uncle Bob starts talking about the total number of parameters that a function could take as inputs before it no longer appears 'clean'. The aim here is to make sure that the functions we write as developers are easy to read, and logical to use. So the argument is that having to input 3 or more parameters for a given function increases the complexity of the function, slowing down the speed in which a developer can read and understand its purpose, as well as increasing the risk of incorrect usage (especially for the vanilla JS / non-typescript projects out there).
This sparked me into thinking about how I regularly handle user alerts within my React applications, and how perhaps a little help from Uncle Bob I could clean my code up a little.
The Original Approach
So here's what we are working on. A simple React application with an AlertContext component that wraps the entire application. This AlertContext component would control the state of alerts generated, and render out a snackbar/toast style alert object at the bottom of the screen.
Because this component uses Reacts Context each of the child components within the AlertContext provider would have the ability to use the alert context and generate success, warning, or error alerts for the user as needed. To keep things simple, I'm just using three buttons in their own component. Each of which generates a different type of alert.
Here's a snapshot of the original AlertContext component.
// AlertContext.tsx
import React from "react";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";
...
const AlertContext = React.createContext<IAlertContext>({
setAlertState: () => {}
});
const AlertProvider: React.FC = ({ children }) => {
const [alertState, setAlertState] = React.useState<IAlertState>({
open: false,
severity: "success",
message: "Hello, world!"
});
const handleClose = (e: React.SyntheticEvent) => {
setAlertState((prev) => ({ ...prev, open: false }));
};
return (
<AlertContext.Provider value={{ setAlertState }}>
{children}
<Snackbar open={alertState.open}>
<MuiAlert onClose={handleClose} severity={alertState.severity}>
{alertState.message}
</MuiAlert>
</Snackbar>
</AlertContext.Provider>
);
};
export { AlertContext, AlertProvider };
Here you can see I have a simple Alert being rendered using the Material-UI Snackbar, and MuiAlert components.
// AlertContext.ts
return (
<AlertContext.Provider value={{ alertSuccess, alertError, alertWarning }}>
{children}
<Snackbar open={alertState.open}>
<MuiAlert onClose={handleClose} severity={alertState.severity}>
{alertState.message}
</MuiAlert>
</Snackbar>
</AlertContext.Provider>
);
This is then controlled by the alertState
object which determines whether the alert is visible
, the severity
of the alert, and the message
that should be displayed.
// AlertContext.ts
const [alertState, setAlertState] = React.useState<IAlertState>({
open: false,
severity: "success",
message: "Hello, world!"
});
The AlertContext component then provides access to the setAlertState
method, allowing any child component that uses the AlertContext to show success, warning, and error type alert messages. For example, here we have a component with three buttons. Each of which when clicked will generate a different type of alert with a different message.
// AlertButtons.ts
import React from "react";
import { Button } from "@material-ui/core";
import { AlertContext } from "./AlertContext";
const AlertButtons = () => {
const { setAlertState } = React.useContext(AlertContext);
const handleSuccessClick = () => {
setAlertState({
open: true,
severity: "success",
message: "Successfull alert!"
});
};
const handleWarningClick = () => {
setAlertState({
open: true,
severity: "warning",
message: "Warning alert!"
});
};
const handleErrorClick = () => {
setAlertState({
open: true,
severity: "error",
message: "Error alert!"
});
};
return (
<div>
<Button variant="contained" onClick={handleSuccessClick}>
Success Button
</Button>
<Button variant="contained" onClick={handleWarningClick}>
Warning Button
</Button>
<Button variant="contained" onClick={handleErrorClick}>
Error Button
</Button>
</div>
);
};
export default AlertButtons;
To show an alert we must first access the setAlertState method from our context provider.
// AlertButtons.tsx
const { setAlertState } = React.useContext(AlertContext);
We can now use this method inside of our onClick functions for each button, or inside any other function, we create. Here, any time a user clicks on the Success Button we will simply generate a success style alert with the message "Successful alert!"
// AlertButtons.tsx
const handleSuccessClick = () => {
setAlertState({
open: true,
severity: "success",
message: "Successfull alert!"
});
};
The cleaner approach
Honestly, there probably isn't much of a problem with the initial approach. Technically, the setAlertState method only requires one parameter... it just happens to be an object with three distinct properties. And if you look closely, you'd see that one of the properties, 'open', isn't actually changing each time we invoke it to show a new alert state. Still, this approach might be just fine if it's just me working on the project, and I understand how to call this method each time. But what if I collaborate with another developer? How clean does the method setAlertState(params: {...}) appear to a new set of eyes?
So my attempt at a cleaner approach then is to change the way we'd set a new alert from the AlertContext component. Instead of giving each of the child components direct access to the setAlertState function of the context, I will give instead provide access to 3 separate methods for each alert type being generated.
// AlertContext.tsx
type IAlertContext = {
alertSuccess: (message: string) => void,
alertError: (message: string) => void,
alertWarning: (message: string) => void,
};
These methods will only take one single parameter, the message, and completely abstracts away the need to remember to set the alert state to open, and to utilize the correct severity type for the alert. Below you can see that we have created the 3 respective methods to alertSuccess()
, alertWarning()
, and alertError()
. Each of which takes a simple message as its input, and internally each function will call setAlertState
with the appropriate open state and severity type.
// AlertContext.tsx
import React from "react";
import Snackbar from "@material-ui/core/Snackbar";
import MuiAlert from "@material-ui/lab/Alert";
type IAlertState = {
open: boolean,
severity: "success" | "warning" | "error",
message: string,
};
type IAlertContext = {
alertSuccess: (message: string) => void,
alertError: (message: string) => void,
alertWarning: (message: string) => void,
};
const AlertContext = React.createContext<IAlertContext>({
alertSuccess: () => {},
alertError: () => {},
alertWarning: () => {}
});
const AlertProvider: React.FC = ({ children }) => {
const [alertState, setAlertState] = React.useState<IAlertState>({
open: false,
severity: "success",
message: "Hello, world!"
});
const handleClose = (e: React.SyntheticEvent) => {
setAlertState((prev) => ({ ...prev, open: false }));
};
const alertSuccess = (message: string) => {
setAlertState({
open: true,
severity: "success",
message: message
});
};
const alertError = (message: string) => {
setAlertState({
open: true,
severity: "error",
message: message
});
};
const alertWarning = (message: string) => {
setAlertState({
open: true,
severity: "warning",
message: message
});
};
return (
<AlertContext.Provider value={{ alertSuccess, alertError, alertWarning }}>
{children}
<Snackbar open={alertState.open}>
<MuiAlert onClose={handleClose} severity={alertState.severity}>
{alertState.message}
</MuiAlert>
</Snackbar>
</AlertContext.Provider>
);
};
export { AlertContext, AlertProvider };
Now back inside our button component, we no longer access the original setAlertState method. Instead, we can access our new alertSuccess()
, alertWarning()
, and alertError()
functions.
const { alertSuccess, alertError, alertWarning } = React.useContext(
AlertContext
);
And then updating each of the respective onClick handlers to call the newly imported functions.
const handleSuccessClick = () => {
alertSuccess("Successfull alert!");
};
const handleWarningClick = () => {
alertWarning("Warning alert!");
};
const handleErrorClick = () => {
alertError("Error alert!");
};
Was it worth it?
To me, the second approach does seem a lot cleaner and is something I'll more than likely stick with in the future. Using the second approach allows me to simply extend the AlertContext to include more severity types than I already have implemented without affecting my implementations throughout any child components. The second approach is surely much easier for any developer who stumbled across the codebase to understand the purpose and use of a method like alertSuccess(message: string)
as opposed to a method called setAlertState(params: {...})
.
Top comments (0)