What Are We Doing?
For the last few months I've been working on an education based project, and one of the challenges/decisions I had to make was how I was going to handle showing alerts once certain action were completed. There is a lot of content creation involved, so I wanted clear notifications that some action was successful (or not).
In general I wanted something super simple that just worked to start, but something I could expand later if I wanted to. For starters I just wanted to be able to do the following:
- Have the alert persist if the page changed, in part so I could send the user to another page and not have to worry about the alert disappearing.
- Look different depending on whether it's responding to a successful action or some error that came up.
- Contain some basic information about what happened, and be able to customize it.
The Solution
I decided to use React Context to handle the alerts. If you've ever used context before you're probably like, "wow, shocker," but I hadn't seen anything online where someone was making alerts with context before. Usually what I found involved packages and things getting passed back and fourth in server requests, and I didn't want a package, I wanted my own deal.
So, my solution involved essentially 3 different things:
- A context High Order Component that wraps the pages in my application with the context provider. (I'm building in Next.js)
- Using the useContext hook to trigger the alert from pages throughout my application.
- An Alert component that is shown/hidden based on the state of my HOC.
Let's look at each part so you can mimic/criticize my method ๐.
Context Provider HOC
If you're new to React Context, it essentially works on the premise that you can (1) create a context object that hold some type of state, (2) provide the context object to your applications using the context provider, and (3) use the context consumer to read the data from the context provider anywhere in your applications. It's a great way to provide application-wide state without having to pass props from one component to another component a half dozen times.
Below I have my High Order Component:
import React, { useState } from 'react';
import { AlertStatus } from '../../lib/enums';
const AlertContext = React.createContext(null);
AlertContext.displayName = 'AlertContext';
const AlertProvider = ({ children }) => {
const [alert, setAlert] = useState(AlertStatus.None);
const [alertText, setAlertText] = useState(null);
return (
<AlertContext.Provider
value={{
alert: alert,
alertText: alertText,
success: (text: string, timeout: number) => {
setAlertText(text);
setAlert(AlertStatus.Success);
setTimeout(() => {
setAlert(AlertStatus.None);
}, timeout * 1000 || 10000)
},
error: (text: string, timeout: number) => {
setAlertText(text);
setAlert(AlertStatus.Error);
setTimeout(() => {
setAlert(AlertStatus.None);
}, timeout * 1000 || 10000)
},
clear: () => (setAlert(AlertStatus.None)),
}}
>
{children}
</AlertContext.Provider>
);
};
export { AlertProvider };
export default AlertContext;
Working through the above code, these are the main points:
Making the Provider Component
- First I create the AlertContext and set the display name (for debugging)
- Next, I create the Alert Provider component. With react context, you wrap up the application (or what parts of your app you want the context available to) in the AlertContext.Provider. The
.Provider
is a component available on all Context objects. So, I am essentially passing in{children}
to my AlertContext.Provider so I can warp up whatever parts of my app in just a<AlertProvider>
component, and it will have the context.
The Context value
React Context takes a 'value' which is the values that the context consumer can read. I've got 5 different aspects of my Alert value.
alert
is a simple piece of state (using the useState hook) that is either set to "SUCCESS", "ERROR", or "NONE". In the code you'll notice that it'sAlertStatus.None
which is because I'm using typescript. But basically AlertStatus.None is equivalent to "NONE". You could use strings just as easily but I'm dipping my toes into TS so that's where I'm at.alertText
is a string that contains the text that will be shown in the alert. It's also just a piece of simple state set with the useState hook.success
is a method that accepts a string (and optionally a number value). This method changes the value ofalertText
, and then sets the alert to "SUCCESS". The optional number value determines how many seconds will elapse before theclear
method runs.error
is the same thing as the success, but it set the alert to "ERROR".clear
is a method that just sets the alert to "NONE".
Using the Context Provider HOC in App
I am using Next.js for my application, so I have a custom _app.jsx that has the main structure of my application. Below you can see the entire thing so far.
function MyApp({ Component, pageProps }) {
return (
<ThemeProvider theme={theme}>
<UserProvider>
<AlertProvider>
<ModalProvider>
<Global styles={global} />
<Head>
<script src="https://apis.google.com/js/platform.js" async defer></script>
</Head>
<div css={layout}>
<NavPanel />
<main>
<ComponentWithRoles Component={Component} pageProps={pageProps} />
<Alert />
<Modal />
</main>
</div>
</ModalProvider>
</AlertProvider>
</UserProvider>
</ThemeProvider>
)
}
The most import thing to note is that we have the AlertProvider (along with a few other Providers) that is wrapping up the application. I could probably just wrap up the <main>
, but currently I have it wrapping most everything up, which makes the context available to every component within the <AlertProvider>
. Nice.
Triggering Alerts!
Now the best part, triggering alerts!
Ok, so in my application I am using GraphQL and Apollo, so below I have an example of a Mutation for enrolling a student in a class. If you're not familiar with Apollo or GraphQL, essentially the mutation is just the part of the logic that's writing information to the database. Within the useMutation hook, there is an object that allows you to do something once the operation has completed, or if there is an error.
import AlertContext from '../context/AlertContext';
const EnrollForm = () => {
const alert = useContext(AlertContext);
const [enroll, { data }] = useMutation(ENROLL_MUTATION, {
onCompleted: (data) => {
alert.success(`Successfully enrolled in class!`)
},
onError: (data) => (alert.error(`Ooops, looks like there was a problem. ${data}`)),
}
)
... all the other stuff...
}
So, really the only important parts are:
- Import the AlertContext. This is exported from the initial HOC component we made.
- Use the React useContext hook to access the Alert Context (which we have access to because it's provided to our component way up in the component tree).
After that, you can call the methods that were made in the context object! So in the Apollo useMutation, you are able to execute a callback if the mutation was successful or not. So, within the onCompleted and onError in the Apollo mutation hook (or any other place you would want) you can simply call alert.success, alert.error, or alert.clear from the context! Calling the methods changes the alert
value of the context to the "SUCCESS", "ERROR", or "NONE" which we can use to show an actual alert.
Actually Showing Alerts
So, we have logic for alerts set up...but what's actually showing up as an alert?!
For this, I have another Component, the <Alert>
component. You may have noticed it earlier from above in the entire ._app.
Here is an ultra simplified version without any styling:
const Alert = () => {
const alert = useContext(AlertContext);
if (alert.alert !== 'NONE') {
return <p>Hey there, I'm the alert! {alert.alertText}</p>
} else {
return null;
}
}
First, you gotta pull in the AlertContext using the useContext hook, just like when you want to trigger the alert methods.
After that we can conditionally render an alert by checking on alert.alert
. Remember, that would be either "SUCCESS", "ERROR", or "NONE". So if the value is not "NONE", the component renders. If the alert.alert
value is "NONE" then null is returned, so nothing shows.
The default methods in the alert context will always call the .clear()
method after 10 seconds if nothing else is specified, which will make the alert disappear, but in my actual component I also include a button to close the alert manually using the context.clear(), just like using the success and error methods. Another thing I do in the real thing is render different styles based on whether or not the alert.alert
is returning "SUCCESS" or "ERROR".
So, that's it! Feel free to leave thoughts/ideas!
Top comments (1)
I've just created this account to say thank you.
This is a great tutorial, easy to follow, well explained, worked like a charm!