React Context API
Let's create a reusable Snackbar and learn to use the Context API effectively!
What's Context?
Context is designed to share data that can be considered “global” for a tree of React components, such as the current authenticated user, theme, or preferred language.
It is made of 2 essential parts - a Provider and a Consumer. The value passed to the Provider can be accessed via Consumer deeper in the component tree.
Initial setup
Let's create a new react app by locating to our desired folder and type
npx create-react-app context-demo
We should clean up the files that we won't be using - .css files, images, etc.
Don't forget to remove their imports as well - check for the index.js and App. files.
We only wish to keep App.js and index.js files in the /src directory.
The result App.js after clean-up shoud look like this
import React from 'react';
function App() {
return (
<div className="App">
<header className="App-header">
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
src/App.js
I've chosen Material UI for the Snackbar component because of wide usage and thorough documents.
We'll be using the core components and the icons.
Install them by typing the following inside your project's directory.
npm install @material-ui/core @material-ui/icons --save
You can check your package.json file to make sure the dependencies have been added.
Continue by creating a components folder and 2 components inside:
- <Content />
- <Snackbar />
They can stay empty for now.
The Snackbar component is where all the displaying logic will be handled.
Content is a component from which we will trigger the snackbar.
src/components/Snackbar.js
src/components/Content.js
import React from 'react';
const Snackbar = () => {
return (
<div></div>
);
}
export default Snackbar;
import React from 'react';
const Content = () => {
return (
<div></div>
);
}
export default Content;
import React from 'react';
import Snackbar from './components/Snackbar';
import Content from './components/Content';
function App () {
return (
<div className="App">
<Content/>
<Snackbar />
</div>
);
}
export default App;
src/App.js
Simple snackbar
(Not connected to context yet)
We'll be using Material UI's "Simple Snackbar" demo and altering it to our needs.
https://material-ui.com/components/snackbars/#simple-snackbars
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import Snackbar from '@material-ui/core/Snackbar';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
const useStyles = makeStyles(theme => ({
close: {
padding: theme.spacing(0.5),
},
}));
export default function SimpleSnackbar() {
const classes = useStyles();
const [open, setOpen] = useState(false);
const handleClick = () => {
setOpen(true);
};
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setOpen(false);
};
return (
<div>
<Button onClick={handleClick}>Open simple snackbar</Button>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={open}
autoHideDuration={6000}
onClose={handleClose}
message={<span>Note archived</span>}
action={[
<IconButton
key="close"
color="inherit"
className={classes.close}
onClick={handleClose}
>
<CloseIcon />
</IconButton>,
]}
/>
</div>
);
}
The result after removing all the unnecessary code should look like this
src/components/Snackbar.js
Replace your empty Snackbar component with the code snippet above and refresh your app.
For now opening the Snackbar is done inside the Snackbar component.
Our goal is to achieve this functionality using Context so we can trigger Snackbar from anywhere in our application.
We can remove the useState() and handleClick() functionality as that will be outsourced to the SnackbarContext.
import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Snackbar from '@material-ui/core/Snackbar';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
const useStyles = makeStyles(theme => ({
close: {
padding: theme.spacing(0.5),
},
}));
export default function SimpleSnackbar() {
const classes = useStyles();
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
// setOpen(false);
};
return (
<div>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={false}
autoHideDuration={6000}
onClose={handleClose}
message={<span>Note archived</span>}
action={[
<IconButton
key="close"
color="inherit"
className={classes.close}
onClick={handleClose}
>
<CloseIcon />
</IconButton>,
]}
/>
</div>
);
}
src/components/Snackbar.js
SnackbarContext
and creating new React Context
As you already read, we have to create a new Context.
The question is - what data's going to be passed to the Provider? What do we need available?
The answer:
- We need some snackbar data object that's going to hold the properties the <Snackbar /> component is interested in - that means the message we want to display, perhaps a type, which will be the snackbar's color, perhaps a custom timeout duration
- a setSnackbar() handler that will be used to control the snackbar's state
Practice dive
Let's create new context and a component that holds the snackbar's state the context provider.
Create a context/ folder in your src/ foler and a SnackbarContext.js file inside it.
Proceed to create a new Context inside it like this:
import React, { createContext } from 'react';
export const SnackbarContext = createContext({
setSnackbar: () => {
},
snackbar: {}
});
As you can see, the initial context value is an empty snackbar object and a setSnackbar() function with an empty body. We're going to be setting this inside a new SnackbarContainer component.
src/context/SnackbarContext.js
SnackbarContainer component
export const SnackbarContainer = ({ children }) => {
return (
<SnackbarContext.Provider value={{}}>
{children}
</SnackbarContext.Provider>
)
};
Let's create a component inside the SnackbarContext.js that will wrap all the other components that will use the Snackbar.
It accepts a children prop as it's going to be used as a wrapper.
It's made of SnackbarContext.Provider (which we created before) to which we're passing an empty object {} value for now.
src/context/SnackbarContext.js
Using SnackbarContainer component
import React from 'react';
import Snackbar from './components/Snackbar';
import Content from './components/Content';
import { SnackbarContainer } from './context/SnackbarContext';
function App () {
return (
<SnackbarContainer>
<div className="App">
<Content />
<Snackbar />
</div>
</SnackbarContainer>
);
}
export default App;
src/App.js
Let's import the <SnackbarContainer /> and wrap the content of the <App /> inside it. Those are the children elements that SnackbarContainer will eventually receive. Now everything inside SnackbarContainer will be able to work with the context's value.
DevTools check
Your component tree using React DevTools should look like this. As you can see, the Provider's value is an empty object because of what we're passing inside in the <SnackbarContainer /> component to the SnackbarContext.Provider.
npm run start
Enhancing the <SnackbarContainer />
Now, we want to pass real values to the Context.Provider - like the snackbar state and a setter function. We handle it just like any functional component's state manipulation - we use the useState() hook provided by React to store and set the snackbar state.
We also define a handleSnackbarSet() function that will get propagated lower and the other components will be able to use it - passing a message and an optional type as arguments.
Enhancing the <SnackbarContainer />
import React, { createContext, useState } from 'react';
export const SnackbarContainer = ({ children }) => {
const [snackbar, setSnackbar] = useState({
message: '',
type: 'default'
});
const handleSnackbarSet = (message, type = 'default') => {
setSnackbar({
message, type
})
};
const contextValue = {
setSnackbar: handleSnackbarSet,
snackbar
};
return (
<SnackbarContext.Provider value={contextValue}>
{children}
</SnackbarContext.Provider>
)
};
src/context/SnackbarContext.js
Your resulting SnackbarContext.Provider value should now contain both snackbar and setSnackbar() properties
Connecting the SnackbarContext to <Content />
We want to trigger the Snackbar popup from anywhere inside our application.
That's why we created a <Content /> component that will reach to <SnackbarContainer /> through the Context and change the component's state.
First, let's create a basic skeleton.
There's a MUI Button that's supposed to open the Snackbar once we click on it.
Let's import the Button, create an empty handleClick function and bind it to the button.
import React from 'react';
import Button from '@material-ui/core/Button';
const Content = () => {
const handleClick = () => {};
return (
<div>
<Button onClick={handleClick}>Open simple snackbar</Button>
</div>
);
};
export default Content;
src/components/Content.js
import React, { useContext } from 'react';
import Button from '@material-ui/core/Button';
import { SnackbarContext } from '../context/SnackbarContext';
const Content = () => {
const { setSnackbar } = useContext(SnackbarContext);
const handleClick = () => {
console.log(setSnackbar);
setSnackbar('Message test');
};
return (
<div>
<Button onClick={handleClick}>Open simple snackbar</Button>
</div>
);
};
export default Content;
We access the context's value using a useContext() hook provided by React. We import the SnackbarContext so we can pass it as an argument to the useContext() hook and retrieve the context's value.
In the <Content /> component, we're only interested in the setSnackbar() function, not in the whole context's value, which also includes snackbar.
When you click the Button, nothing seems to happen.
That's because we didn't bind our <Snackbar /> component to the Context yet, but that's okay!
We can always use DevTools to verify that we indeed did successfully use Context to set a new snackbar state.
Remember the Context is only providing you with a function that's actually located in the <SnackbarContainer /> component.
Connecting the SnackbarContext to <Snackbar />
The Snackbar needs to listen to the SnackbarContainer state changes and act accordingly.
That's why we created a <Snackbar /> component that will reach to <SnackbarContainer /> through the Context and display a message if it appears.
As we already have a basic Snackbar layout, we reach to the SnackbarContext using react.useContext() the same way as in the <Content /> component.
We care about the snackbar property from the context's value, which gives us an information whether there's a message (and a type, which we won't use for now). We can already choose to display the message conditionally.
import React, { useContext } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Snackbar from '@material-ui/core/Snackbar';
import IconButton from '@material-ui/core/IconButton';
import CloseIcon from '@material-ui/icons/Close';
import { SnackbarContext } from '../context/SnackbarContext';
const useStyles = makeStyles(theme => ({
close: {
padding: theme.spacing(0.5),
},
}));
export default function SimpleSnackbar() {
const classes = useStyles();
const { snackbar } = useContext(SnackbarContext);
console.log('snackbar value', snackbar)
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
// setOpen(false);
};
const { message } = snackbar;
return (
<div>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={!!message}
autoHideDuration={6000}
onClose={handleClose}
message={message ? <span>{message}</span> : null}
action={[
<IconButton
key="close"
color="inherit"
className={classes.close}
onClick={handleClose}
>
<CloseIcon />
</IconButton>,
]}
/>
</div>
);
}
src/components/Snackbar.js
If you click the button, the Snackbar message should already be displayed.
It won't go away though, as the boolean open property is set to be true whenever we have a truthy message value - and as we're not removing the message, the Snackbar stays open forever.
Notice the // setOpen(false) line that we commented out in the handleClose() method.
We can use the same setSnackbar() function that we're using in the <Content /> component to set the message to an empty string (or null, if you like).
const { snackbar, setSnackbar } = useContext(SnackbarContext);
console.log('snackbar value', snackbar)
const handleClose = (event, reason) => {
if (reason === 'clickaway') {
return;
}
setSnackbar('');
};
src/components/Snackbar.js
Because we bound the displayed message logic to a truthy message property in the snackbar object, setting the message to '' will hide our snackbar again.
Watch the console logs to see how our SnackbarContainer state changes.
Using various snackbar colors
Let's adjust the <Content /> component to display more snackbars and make use of the second type argument of the setSnackbar() method
const Content = () => {
const { setSnackbar } = useContext(SnackbarContext);
const handleClick = type => {
setSnackbar(`Message test ${type}`, type);
};
return (
<div>
<Button onClick={() => handleClick('success')}>Open success snackbar</Button>
<Button onClick={() => handleClick('error')}>Open error snackbar</Button>
<Button onClick={() => handleClick('info')}>Open info snackbar</Button>
<Button onClick={() => handleClick('warning')}>Open warning snackbar</Button>
</div>
);
};
src/components/Content.js
We won't see the result just yet as we didn't set the <Snackbar /> up to change colors, but we can always check the DevTools or console.log() to make sure that we didn't only set a new message property, but also a type property.
To have the <Snackbar /> accept various colors, let's check the MUI docs again - https://material-ui.com/components/snackbars/#customized-snackbars
import { green, pink, blue, amber } from '@material-ui/core/colors';
const useStyles = makeStyles(theme => ({
success: {
backgroundColor: green[600],
},
error: {
backgroundColor: pink[600],
},
info: {
backgroundColor: blue[600],
},
warning: {
backgroundColor: amber[700],
},
close: {
padding: theme.spacing(0.5),
},
}));
src/components/Snackbar.js
And let's import the colors and add corresponding classes.
import SnackbarContent from '@material-ui/core/SnackbarContent';
return (
...
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={!!message}
autoHideDuration={6000}
onClose={handleClose}
>
<SnackbarContent
className={classes[type]}
message={message ? <span>{message}</span> : null}
action={[
<IconButton
key="close"
color="inherit"
onClick={handleClose}
>
<CloseIcon />
</IconButton>
]}
/>
</Snackbar>
...
)
src/components/Snackbar.js
Based on the MUI docs, let's import SnackbarContent to color the Snackbar using className={classes[type]}. We can remove the message={...} and action=[...] attributes from the MUI <Snackbar /> component as they're provided inside <SnackbarContent /> now.
Almost done?
Our Snackbar is working and it can be toggled from anywhere.
But what if there are some optimizations we could do?
The <Content /> is re-rendering
const Content = () => {
const { setSnackbar } = useContext(SnackbarContext);
const handleClick = type => {
setSnackbar(`Message test ${type}`, type);
};
console.log('Content re-render');
return (
<div>...</div>
);
};
If you put a console log inside your <Content /> component and click the buttons, you can see it's being logged every time.
Do you think the <Content /> has a real reason to re-render all over just a because a message is being shown and hidden?
src/components/Content.js
Some of you might've already guessed it - it's the context that's causing the re-renders.
We're setting this value each time we update the snackbar, although we're not really updating the setter function at all.
I'm asking - How to optimize this? How to limit wasted render checks?
Just like a state or a prop change triggers a component's re-render, so does a context's value change.
We're updating the SnackbarContainer each time we set a new snackbar state.
That means the contextValue object gets created anew, the SnackbarContext.Provider value is changed and everything that's consuming the value gets re-rendered as well, even if it's using just the setter function.
src/context/SnackbarContext.js
export const SnackbarContainer = ({ children }) => {
const [snackbar, setSnackbar] = useState({...});
const handleSnackbarSet = (message, type = 'default') => {...};
const contextValue = {
setSnackbar: handleSnackbarSet,
snackbar
};
return (
<SnackbarContext.Provider value={contextValue}>
{children}
</SnackbarContext.Provider>
)
};
The solution to bloated contexts that cause re-renders
Is to use multiple contexts!
src/context/SnackbarContext.js
Let's create separate contexts for Snackbar value and for Snackbar setting.
The philosophy behind this is that even if you change the snackbar object value, you're not changing the setter function - and the context that's providing you with it won't cause re-renders, because its value won't be updated.
Careful: this will break our app the components will throw an error if you remove the initial context. You can ignore it for now.
src/context/SnackbarContext.js
import React, { createContext, useState } from 'react';
export const SnackbarValueContext = createContext({});
export const SnackbarSetContext = createContext(() => {});
export const SnackbarContainer = ({ children }) => {
const [snackbar, setSnackbar] = useState({
message: '',
type: 'default'
});
const handleSnackbarSet = (message, type = 'default') => {
setSnackbar({
message, type
})
};
return (
<SnackbarValueContext.Provider value={snackbar}>
<SnackbarSetContext.Provider value={handleSnackbarSet}>
{children}
</SnackbarSetContext.Provider>
</SnackbarValueContext.Provider>
)
};
src/context/SnackbarContext.js
import React, { createContext, useState, memo } from 'react';
export const SnackbarValueContext = createContext({});
export const SnackbarSetContext = createContext(() => {});
const SnackbarProvider = memo(({ setSnackbar, children }) => {
console.log('SnackbarProvider refresh');
const handleSnackbarSet = (message, type = 'default') => {
setSnackbar({
message, type
})
};
return (
<SnackbarSetContext.Provider value={handleSnackbarSet}>
{children}
</SnackbarSetContext.Provider>
)
});
export const SnackbarContainer = ({ children }) => {
const [snackbar, setSnackbar] = useState({
message: '',
type: 'default'
});
return (
<SnackbarValueContext.Provider value={snackbar}>
<SnackbarProvider setSnackbar={setSnackbar}>
{children}
</SnackbarProvider>
</SnackbarValueContext.Provider>
)
};
We also need to create a separate Provider component, so we can wrap it in react.memo(). Notice we also moved the handleSnackbarSet() method there.
The memo() is a hoc for functional components that serves the same purpose as PureComponent does - to do a shallow comparison between previous props and next props and decide (not) to re-render the component wrapped inside it.
This way even if the <SnackbarContainer /> updates, when it's trying to render <SnackbarProvider />, it won't re-fresh because none of its props change and memo helps us to compare it and deny the attempt to re-render in vain.
Be careful about future issues
We have two different contexts now. Imagine how hard importing and using them gets as your app grows.
That's why we can create custom hooks to separate our context logic from our component logic and just use the hook.
That way we keep the app modular and we just change logic inside the hooks and don't have to touch the components at all.
Accessing the snackbar methods via a hook
If you ever decide to switch the technology, the only file you need to change is the hook providing you with snackbar logic.
src/hooks/useSnackbar.js
import { useContext } from 'react';
import { SnackbarSetContext, SnackbarValueContext } from '../context/SnackbarContext';
const useSetSnackbar = () => {
const setSnackbar = useContext(SnackbarSetContext);
return setSnackbar;
};
const useGetSnackbar = () => {
const snackbar = useContext(SnackbarValueContext);
return snackbar;
};
const useSnackbar = () => {
const setSnackbar = useContext(SnackbarSetContext);
const snackbar = useContext(SnackbarValueContext);
return {
setSnackbar,
snackbar
}
};
export {
useSetSnackbar,
useGetSnackbar,
useSnackbar
};
Here we can access the context and export their values as hooks. We can also create a useSnackbar() hook that joins values from both contexts.
Be careful about not using the useSnackbar() if you just need the setter. Because it's combined with snackbar value, it keeps getting re-created.
src/hooks/useSnackbar.js
import { useContext } from 'react';
import { SnackbarSetContext, SnackbarValueContext } from '../context/SnackbarContext';
const useSetSnackbar = () => useContext(SnackbarSetContext);
const useGetSnackbar = () => useContext(SnackbarValueContext);
const useSnackbar = () => {
const setSnackbar = useContext(SnackbarSetContext);
const snackbar = useContext(SnackbarValueContext);
return {
setSnackbar,
snackbar
}
};
export {
useSetSnackbar,
useGetSnackbar,
useSnackbar
};
We can also shorten the code a little.
src/components/Snackbar.js
import { useSnackbar } from '../hooks/useSnackbar';
export default function SimpleSnackbar () {
const classes = useStyles();
const { snackbar, setSnackbar } = useSnackbar();
const handleClose = (event, reason) => {...};
const { message, type } = snackbar;
return (
<div>...</div>
);
}
Of course we need to use the new hook in the <Snackbar /> component in order to get the buttons working again.
We can either use the useSnackbar() hook or use both useSetSnackbar() and useGetSnackbar() separately.
src/components/Content.js
import React from 'react';
import Button from '@material-ui/core/Button';
import { useSetSnackbar } from '../hooks/useSnackbar';
const Content = () => {
const setSnackbar = useSetSnackbar();
const handleClick = type => {
setSnackbar(`Message test ${type}`, type);
};
console.log('Content re-render');
return (
<div>
<Button onClick={() => handleClick('success')}>Open success snackbar</Button>
<Button onClick={() => handleClick('error')}>Open error snackbar</Button>
<Button onClick={() => handleClick('info')}>Open info snackbar</Button>
<Button onClick={() => handleClick('warning')}>Open warning snackbar</Button>
</div>
);
};
export default Content;
Let's access the useSetSnackbar() hook in our <Content /> component and try it out!
If we've done everything well, you shouldn't see the console.log() that's alerting us of attempted re-render again.
Conclusion
Context is great when used wisely!
But remember that if you use it instead of global state management (redux, mobx) and you're using only a single context instance
You're bundling all the logic into a single object that gets passed to the provider and you're most likely giving your components a hard time with checking whether they should render or not.
Don't be afraid to use multiple contexts. Monitor your performance, put console.logs everywhere - you might not have a clue about all the components that are being updated just because they're connected to your giant context.
React Context API - create a reusable Snackbar!
By Dana Janoskova
React Context API - create a reusable Snackbar!
- 5,588