yarik
Mar 23 2020 at 22:35 GMT
I would like to know how to implement toast notifications in React using the modern React hooks API.
Specifically, I would like to have a useToasts
hook that returns an object or array that exposes an addToast
function that allows me to add a toast:
const { addToast } = useToasts()
yarik
Mar 23 2020 at 23:44 GMT
In order to have toasts, we need to store their state somewhere and be able to easily modify it (i.e., add new toasts).
We can achieve this by having a component that wraps our app and keeps track of all the toasts that are shown. At the same time, it allows adding new toasts by exposing an addToast
function via the Context API. Let's call this component ToastsProvider
.
<ToastsProvider>
<App />
</ToastsProvider>
Then, in a component inside our app, we can do
const { addToast } = useToasts()
const handleClick = useCallback(() => {
addToast('This is a new toast notification!'))
}, [addToast])
return <button onClick={handleClick}>
I'll now walk you through on how to implement this.
ToastsContext
Let's start by creating the context:
const ToastsContext = React.createContext({
addToast: () => {
throw new Error('To add a toast, wrap the app in a ToastsProvider.')
}
})
Notice that we provide the default context value, which is used when we try to access the context value without a provider up in the React tree, with an addToast
function that throws a meaningful error when called.
ToastsProvider
Next, let's actually implement ToastsProvider
:
const ToastsProvider = ({ children }) => {
const [toasts, setToasts] = useState([])
const addToast = useCallback((content, options = {}) => {
const { autoDismiss = true } = options
const toastId = getUniqueId()
const toast = {
id: toastId,
content,
autoDismiss,
remove: () => {
setToasts((latestToasts) => latestToasts.filter(({ id }) => id !== toastId))
}
}
setToasts((latestToasts) => [ ...latestToasts, toast ])
}, [])
const contextValue = useMemo(() => ({
addToast,
}), [addToast])
return (
<ToastsContext.Provider value={contextValue}>
{children}
{/* Render the toasts somehow */}
</ToastsContext.Provider>
)
}
Notice that we allow passing an autoDismiss
option to addToast
, which indicates whether the toast notification should automatically disappear after some time. The default is true
.
The getUniqueId
function above could be something as simple as:
let counter = 0
const getUniqueId = () => `id-${counter++}`
The reason why we want to associate an id with each toast is that we want to be able to easily find and remove a toast from the toasts array. At the same time, we want to have some unique identifier associated with each toast so that we can use it as the key
prop when mapping the array of toasts to the toast components. We should not use the content of the toasts as the key because it may not be unique (multiple toasts can have the same content) and it's not necessarily a string (the content could be a React element).
Notice that above we didn't specify how to render the toast notifications.
We want those notifications to be position fixed inside the document body so that we can have them show up at the top or bottom of the page.
In order to render them inside the document body rather than wherever in the tree they would normally render, we need to use a React portal.
So, the placeholder comment above
{/* Render the toasts somehow */}
would become
{createPortal((
<ToastsContainer>
{toasts.map((toast) => (
<Toast key={toast.id} {...toast} />
))}
</ToastsContainer>
), document.body)}
ToastsContainer
is the component that wraps all our toasts and we want to have it position fixed within the document body.
Let's say that we want our toasts to appear at the bottom right of the screen.
Using styled-components
, we could implement ToastsContainer
as:
const ToastsContainer = styled.div`
position: fixed;
bottom: 0;
right: 0;
width: 100%;
max-width: 400px;
padding: 16px;
`
Toast
componentNow, let's get to the actual Toast
component:
const autoDismissAfterMs = 5000
const Toast = ({ content, autoDismiss, remove }) => {
useEffect(() => {
if (autoDismiss) {
const timeoutHandle = setTimeout(remove, autoDismissAfterMs)
return () => clearTimeout(timeoutHandle)
}
}, [autoDismiss, remove])
return (
<Container onClick={remove}>
{content}
</Container>
)
}
We define an autoDismissAfterMs
variable, which indicates after how much time (in milliseconds) we want the toast to be automatically dismissed.
Then, inside the Toast
component, we call a useEffect
hook in which, if autoDismiss
is true, we set a timer to call remove
after autoDismissAfterMs
milliseconds (5 seconds).
The Container
component that we use above could be a styled component like this:
const Container = styled.div`
padding: 16px;
margin-top: 16px;
background-color: rebeccapurple;
color: white;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
`
Finally, let's see the implementation of the useToasts
hook:
const useToasts = () => {
return useContext(ToastsContext)
}
That's it! We implemented toast notifications using React hooks.
As a next step, you could customize the look and feel of the toast notifications and add animations to them.