What is it? 🤔
A Snackbar is a UI component that provides the user visual feedback on an event within the app without interrupting the user experience. This is typically shown as a message box that informs the user of an action being performed or that will be performed by the app.
Snackbars typically behave in the following manner:
- They provide information about an app process via text.
- They will disappear on their own after a certain time.
- They should not interrupt the user from anything else.
That being said, let's get started with a fresh create-react-app.
Setting Up The Redux Store 💾
For now, we'll create a simply button in our App component to trigger the Snackbar via react-redux
. The setup for the redux store is very straightforward, with all the actions and reducers in the same redux folder as follows.
/* ---- redux/actions.js ---- */
export const toggleSnackbarOpen = (message) => ({
type: "TOGGLE_SNACKBAR_OPEN",
message,
});
export const toggleSnackbarClose = () => ({
type: "TOGGLE_SNACKBAR_CLOSE",
});
For now, we just want to be able to pass a message in our dispatch to be rendered in the Snackbar, other parameters can also be added such as a timer or even the variation of snackbar being rendered i.e. success, warning, or informative, but for now we'll stick with the basics.
/* ---- redux/reducers.js ---- */
const initialState = {
toggleSnackbar: false,
snackbarMessage: null,
};
export default function reducer(state = initialState, action) {
switch (action.type) {
case "TOGGLE_SNACKBAR_OPEN": {
return {
...state,
toggleSnackbar: true,
snackbarMessage: action.message,
};
}
case "TOGGLE_SNACKBAR_CLOSE": {
return {
...state,
toggleSnackbar: false,
snackbarMessage: null,
};
}
default: {
return state;
}
}
}
In our reducers' initial state we would need a message and the boolean state of the Snackbar. We can also include different states for different types of messages like warningMessage
for a toggleWarningSnackbar
state.
/* ---- redux/store.js ---- */
import { createStore } from "redux";
import reducer from "./reducers";
const config =
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();
export default function configureStore(initialState) {
const store = createStore(reducer, initialState, config);
return store;
}
And of course, we create the redux store and configure it, and then we connect the store to the App with the Provider as follows:
/* ---- index.js ---- */
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import { Provider } from "react-redux";
import configureStore from "./redux/store";
const store = configureStore();
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById("root")
);
The Snackbar Component 🍫
For this example, we only want our Snackbar to display a message and fade itself away after an event is fired, but it will also allow the user to dismiss the Snackbar altogether. We will also add a timeout variable as a prop to this component to define when the notification will disappear.
import React, { useEffect } from "react";
import styled, { keyframes } from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { toggleSnackbarClose } from "../redux/actions";
import { FiX } from "react-icons/fi";
const Snackbar = ({ timeout }) => {
const dispatch = useDispatch();
// select the UI states from the redux store
const SHOW = useSelector((state) => state.toggleSnackbar);
const MESSAGE = useSelector((state) => state.snackbarMessage);
// convert the timeout prop to pass into the styled component
let TIME = (timeout - 500) / 1000 + "s";
let TIMER;
function handleTimeout() {
TIMER = setTimeout(() => {
dispatch(toggleSnackbarClose());
}, timeout);
}
function handleClose() {
clearTimeout(TIMER);
dispatch(toggleSnackbarClose());
}
useEffect(() => {
if (SHOW) {
handleTimeout();
}
return () => {
clearTimeout(TIMER);
};
}, [SHOW, TIMER]);
return (
SHOW && (
<Container time={TIME}>
<p>{MESSAGE}</p>
<Button onClick={handleClose}>
<FiX />
</Button>
</Container>
)
);
};
const fadein = keyframes`
from {
bottom: 0;
opacity: 0;
}
to {
bottom: 1rem;
opacity: 1;
}
`;
const fadeout = keyframes`
from {
bottom: 1rem;
opacity: 1;
}
to {
bottom: 0;
opacity: 0;
}
`;
const Container = styled.div`
position: fixed;
z-index: 1000;
bottom: 1rem;
left: 50%;
transform: translateX(-50%);
height: auto;
padding: 0.625rem 1rem;
border-radius: 0.75rem;
border: transparent;
background-color: hsl(200, 100%, 65%);
color: white;
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
display: flex;
justify-content: center;
align-items: center;
animation: ${fadein} 0.5s, ${fadeout} 0.5s ${(props) => props.time};
`;
const Button = styled.button`
display: flex;
justify-content: center;
align-items: center;
margin-left: 0.875rem;
padding: 0;
margin-left: 1rem;
height: 1.75rem;
width: 1.75rem;
text-align: center;
border: none;
border-radius: 50%;
background-color: transparent;
color: white;
cursor: pointer;
&:hover {
background-color: hsl(200, 100%, 60%);
}
`;
export default Snackbar;
To make the Snackbar disappear on its own once toggled we use setTimeout
to trigger another dispatch to close the Snackbar according to the value of the timeout
prop. You will notice that 0.5s was shaved off to the TIME
variable to allow our nice fadeOut
animation to take place, when it is passed as a prop to our Container
component. Note that the keyframes
animations must take precedence before being called into the animation
CSS property
Additionally, we have another button within the Snackbar that displays alongside the message that can close the Snackbar.
The App 🖥️
With reusability in mind, we want to be able to activate the Snackbar just by simply importing its component and its action dispatcher to any view.
/* ---- App.js ---- */
import React from "react";
import GlobalStyles from "./components/GlobalStyles";
import styled from "styled-components";
import Snackbar from "./components/Snackbar";
import { useDispatch, useSelector } from "react-redux";
import { toggleSnackbarOpen } from "./store/actions";
const App = () => {
const dispatch = useDispatch();
return (
<>
<GlobalStyles />
<Wrapper>
<Button
onClick={() => {
dispatch(toggleSnackbarOpen("I'm a Snackbar!"));
}}
>
Click Me!
</Button>
<Snackbar timeout={3000} />
</Wrapper>
</>
);
};
const Wrapper = styled.div`
height: 100vh;
background: #fffc;
display: flex;
justify-content: center;
align-items: center;
`;
const Button = styled.button`
padding: 0.5rem 1rem;
font-size: 1.3rem;
border-radius: 0.5rem;
outline: none;
border: none;
background: lightblue;
cursor: pointer;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);
&:hover {
background: lightgoldenrodyellow;
}
`;
export default App;
And just like that, we have a working (albeit basic) Snackbar that can be re-used in any other component! The code shown so far can also be viewed in this Code Sandbox snippet:
✨ But Wait, There's More! ✨
There are a lot of cool features to add in a Snackbar, such as its anchoring position or having Snackbar variations, for the full specifications, you should definitely check out Material Design's page here.
If you are interested in adding some of those features, please feel free to check out my take on this over here:
Top comments (3)
what about if you have multiple message that you want to show??
Hi there! To handle multiple messages, you would need to update your redux store to manage an array of message objects. You would then
map()
the messages array instead.Hope that helps!