Support Ukraine 🇺🇦Help Ukrainian ArmyHumanitarian Assistance to Ukrainians

How to implement toast notifications in React using hooks?

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()

1 Answer

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.

Creating 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.

Implementing 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;
`

Implementing the Toast component

Now, 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)
}

Conclusion

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.

claritician © 2022