In any application there are always times when you need to relay small pieces of information to the user. In web apps, this is usually done with a toast type message. In mobile apps, this is usually some sort of alert or local notification.
In this tutorial, we are gonna run through how to create reusable alert components that are updated with React context and hooks.
We are gonna working with a simple notes app, you can clone the finished project from here.
You can see that it already has the functionality to view and create notes but we want to notify the user if saving the note was a success or if there was an error. Obviously this is just an example of where the alert can be used. It could be used to notify the user of anything!
In the example code, I am using the awesome React Native Paper. Read my recent article to find out why I use it and why I think it the best React Native UI library. We are going to use the Snackbar
component as our alert component but you could use anything that is appropriate for your project.
We are breaking the article into a few distinct parts.
We are going to use React.context
for holding and setting our alert state. The React documentation states that
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Context is perfect for managing simple pieces of state that need to be available globally.
Global Context Provider
I think it is good practice to split the state that is stored in context based on domain, i.e. having separate contexts for alerts, sidebar, cart etc. By doing this you can avoid unnecessary re-renders, your alert context is not fussed about your sidebar context and so updating one should not re-render components using another.
Context is made available to your application by wrapping your app in a Context.provider
. But this can be a "problem" when you have multiple contexts as it makes your App.js
bloated and slightly more unreadable. But all is not lost, Scott Tolinsky from LevelUp tuts put me onto a great snippet of code that composes your Context.Providers
into a single component. This makes things so much neater.
You have probably worked out that having multiple contexts in you App.js
is not actually a problem. I just like to have things neat and tidy.
import * as React from "react";
// we will import our context providers here
function ProviderComposer({ contexts, children }) {
return contexts.reduceRight(
(kids, parent) =>
React.cloneElement(parent, {
children: kids
}),
children
);
}
function ContextProvider({ children }) {
return (
// we add our providers to the contexts prop
<ProviderComposer contexts={[]}>{children}</ProviderComposer>
);
}
export default ContextProvider;
Alert provider
First, we need to create the context the can hold our alert state. We use React.createContext
and assign it to a variable. Notice that we also export the variable, this means that we can use it later in our other components.
We also create an AlertProvider
component that wraps our AlertContext.Provider
, this is what gives us access to the state stored in our AlertContext
.
import * as React from "react";
export const AlertContext = React.createContext({});
export const AlertProvider = ({ children }) => {
return (
<AlertContext.Provider
value={// Our context values will go here}>
{children}
</AlertContext.Provider>
);
};
Next, we need a way to manage the data stored in our context. We could use React.useState
for this but due to the slightly more complex structure of our data and the fact that we will be updating more than one piece of data to fire our alert component, I decided to use React.useReducer
instead. It makes both the implementation of the Alert provider and the execution of its method so much neater.
[...]
const initialState = {
type: "close",
open: false,
alertType: "info",
message: ""
};
const reducer = (state, action) => {
switch (action.type) {
case "close":
return {
...initialState
};
case "open":
return {
open: true,
alertType: action.alertType,
message: action.message
};
default:
throw new Error("Action not found");
}
};
Finally, we need to put it all together and use our reducer
in our provider
giving us access to all the stored alert state. This combination allows us to update and access any part of the alert state from any part of the app, as long as the app is wrapped in our global context provider.
import * as React from "react";
const initialState = {
type: "close",
open: false,
alertType: "info",
message: ""
};
export const AlertContext = React.createContext({});
const reducer = (state, action) => {
switch (action.type) {
case "close":
return {
...initialState
};
case "open":
return {
open: true,
alertType: action.alertType,
message: action.message
};
default:
throw new Error();
}
};
export const AlertProvider = ({ children }) => {
const [alertState, dispatchAlert] = React.useReducer(reducer, initialState);
return (
<AlertContext.Provider
value={{
alertState,
dispatchAlert
}}>
{children}
</AlertContext.Provider>
);
};
The Alert Component
As I mentioned at the start of this article we are using React Native Paper and its Snackbar component to alert our users of any information in our apps. But this could be swapped out for anything else. You just need a way to consume the data that is being passed down from the alert context.
This component is quite simple. We are using the React.useContext
hook to subscribe to changes to the AlertContext
and then opening/closing the popup based on the state. We set the style of the alert box based on the alertState.alertType
property to properly convey the meaning of the message.
import * as React from "react";
import { Snackbar } from "react-native-paper";
import { AlertContext } from "../globalState";
import { colors } from "../constants";
const SnackBar = () => {
const { alertState, dispatchAlert } = React.useContext(AlertContext);
const [alertSyle, setAlertStyle] = React.useState({
backgroundColor: colors.info
});
React.useEffect(() => {
switch (alertState.alertType) {
case "info":
setAlertStyle({
backgroundColor: colors.success
});
break;
case "error":
setAlertStyle({
backgroundColor: colors.error
});
break;
case "success":
setAlertStyle({
backgroundColor: colors.success
});
break;
default:
setAlertStyle({
backgroundColor: colors.info
});
}
}, [alertState]);
const closeMe = () => {
dispatchAlert({ type: "close" });
};
return (
<>
{typeof alertState.open === "boolean" && (
<Snackbar
style={alertSyle}
visible={alertState.open}
onDismiss={() => closeMe()}
action={{
label: "Undo",
onPress: () => {
console.log("Snackbar closed");
// Do something
}
}}>
{alertState.message}
</Snackbar>
)}
</>
);
};
export default SnackBar;
Using our Alert provider
Finally, we are now in a position to use our sweet new AlertContext
from anywhere in our app. In the example below, we are notifying the user of the outcome of their GraphQL mutation.
If you want to learn how to easily integrate GraphQL into your React Native Application take a look at this article.
In the same way, we did in our Snackbar component, we are using the useContext
hook to gain access to the dispatchAlert
method which will allow us to alert the user to the success or errors in there GraphQL mutation.
[...]
const NoteCreateScreen = ({ navigation }) => {
const { dispatchAlert } = React.useContext(AlertContext);
const createNoteMutation = useMutation(gql(createNote));
return (
<SafeAreaView style={gStyle.container}>
<ScrollView contentContainerStyle={gStyle.contentContainer}>
<View style={{ flex: 1, height: '100%', width: '100%' }}>
<Surface style={styles.surface}>
<Formik
initialValues={{ note: '', title: '' }}
onSubmit={({ note, title }) => {
const input = {
id: uuid(),
title,
note,
createdAt: moment().toISOString()
};
createNoteMutation({
variables: {
input
},
update: (_, { data, error }) => {
if (error) {
dispatchAlert({
type: 'open',
alertType: 'error',
message: 'Error creating note'
});
} else {
dispatchAlert({
type: 'open',
alertType: 'success',
message: 'Note created'
});
navigation.state.params.refetch();
navigation.goBack();
}
}
});
}}
>
{({ values, handleSubmit, handleChange }) => {
return (
<>
[...]
</>
);
}}
</Formik>
</Surface>
</View>
</ScrollView>
</SafeAreaView>
);
};
export default NoteCreateScreen;
And there you have it, a highly customisable, reusable and globally executable local notification system. This type of situation is a perfect use for React Context
but obviously there are so many more.
What other stuff will you create?
Thanks for reading 🙏
If there is anything I have missed, or if there is a better way to do something then please let me know
Top comments (2)
so great thanks man!
No worries