DEV Community

Aibol Kussain
Aibol Kussain

Posted on • Originally published at aibolik.com on

Creating Toast API with React Hooks

You can also read this post on my blog by this link.

In this blog post we will be gradually creating fully working Toast API and we will use advantages of React Hooks to create nicer hooks-supported interface. Full working example is available here.

Example of shown toast notification
Example of shown toast notification

Toast component

Let’s start by creating simple Toast component. It should be simple nice looking box that renders some content. For simplicity of this application, let that content be just a text.

I will use styled-components in this example for styling.

const Wrapper = styled.div`
  margin-right: 16px;
  margin-top: 16px;
  width: 200px;

  position: relative;
  padding: 16px;
  border: 1px solid #d7d7d7;
  border-radius: 3px;
  background: white;
  box-shadow: 0px 4px 10px 0px #d7d7d7;
  color: #494e5c;
`;

const Toast = ({ children }) => (
  <Wrapper>{children}</Wrapper>
);
Enter fullscreen mode Exit fullscreen mode

Now we have basic Toast, you can test it out by rendering <Toast> Example</Toast> in your root component(App.js).

ToastContainer component

Usually, there can be several toasts at the same time and they are positioned at some corner of the page. Therefore, it makes sense to create ToastContainer component, that will be responsible for toasts positioning and rendering them in a sequence.

For simplicity, let’s assume that toast notifications will always be rendered at top right corner. If you want it to be more customisable, ToastContainer is a right place for this.

Additionally, in order to not mess with z-index, it is better to render components like toasts somewhere up in a DOM tree. In our example we will render them directly inside body of the page. We can easily accomplish this using React DOM’s portal API.

const Wrapper = styled.div`
  position: absolute;
  /* Top right corner */
  right: 0;
  top: 0;
`;

const ToastContainer = ({ toasts }) => {
  return createPortal(
    <Wrapper>
      {toasts.map(item => (
        <Toast key={item.id} id={item.id}>{toast.content}</Toast>
      )}
    </Wrapper>,
    document.body
  );
}
Enter fullscreen mode Exit fullscreen mode

Inside of wrapper we render array of toasts. We assume that toasts is an array of objects with id and content keys. id is a unique ID of each toast notification that we will use later to dismiss it, and content is just a text.

ToastProvider

We built Toast and ToastContainer components, but we will not expose them directly. Instead, we will expose them through ToastProvider component, that will be responsible for rendering and managing all toasts. If we were building some library or package, ToastProvider would be the one exported and exposed to its consumers(of course along with hooks).

Since it should hold all toasts, let’s use React’s useState hook to save and manage toasts array.

const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = useState([]);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

ToastProvider will also use React's context API to pass helper functions down the tree: addToast and removeToast.

addToast function

This function should add toast object into toasts array in ToastProvider. So it's usage will look like this: addToast('You friend John liked your photo'). As you can see, it should take a string as an argument, that will end up being content. Assigning of ID will be responsibility of the function, therefore we need some way of tracking unique IDs. For simplicity, we can have global variable id that will be incremented on each function call. Let's see how the function would look:

let id = 0;
const ToastProvider = ({ children }) => {
  // ...

  const addToast = useCallback(content => {
    setToasts(toasts => [
      ...toasts,
      { id: id++, content }
    ]);
  }, [setToasts]);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Note the usage of functional update of setToasts. We need to use that, since new toasts array is computed using previous state.

I used useCallback, as a small optimisation. We don't need to recreate this function on every render, therefore we use useCallback hook. Read more about it in React's hooks documentation.

removeToast function

Contrary to addToast, this function should remove toast object from toasts array in ToastProvider component given the ID of a toast. Guess where this function should be called from... from anywhere where ID is known! Remember we added id prop to Toast component? We will use that id to call removeToast. Let's see this function's code:

const ToastProvider = ({ children }) => {
  // ...

  const addToast = useCallback(content => {
    setToasts(toasts => [
      ...toasts,
      { id: id++, content }
    ]);
  }, [setToasts]);

  const removeToast = useCallback(id => {
    setToasts(toasts => toasts.filter(t => t.id !== id));
  }, [setToasts]);

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Very simple function — we just filter out the dismissed toast by its ID.

We are almost done with ToastProvider component. Let's put everything together and see how it would look:

const ToastContext = React.createContext(null);

let id = 1;

const ToastProvider = ({ children }) => {
  const [toasts, setToasts] = useState([]);

  const addToast = useCallback(content => {
    setToasts(toasts => [
      ...toasts,
      { id: id++, content }
    ]);
  }, [setToasts]);

  const removeToast = useCallback(id => {
    setToasts(toasts => toasts.filter(t => t.id !== id));
  }, [setToasts]);

  return (
    <ToastContext.Provider value={{ addToast, removeToast }}>
      <ToastContainer toasts={toasts} />
      {children}
    </ToastContext.Provider>
  );
}
Enter fullscreen mode Exit fullscreen mode

Nothing new in this code: we just added ToastContext, so that addToast and removeToast can be used anywhere down the React tree. Then we render ToastContainer, that will be rendered always inside body of page, thanks to Portals. And children, since ToastProvider is rendered at the top level of React tree(along with other providers, e.g. Redux's Provider, ThemeProvider, etc.).

useToast hook

Finally we reached to creating our own hook, that will be exported along with ToastProvider. This hook is actually very simple and consists of only 2 lines of code. It's purpose is to make addToast and removeToast available with just a function/hook call. Without this hook, you'd use addToast and removeToast by importing ToastContext and usage of React.useContext:

import { ToastContext } from './path/to/ToastProvider';

const Example = () => {
  const { addToast } = React.useContext(ToastContext);
  // ...
Enter fullscreen mode Exit fullscreen mode

Let’s implement this simple hook:

export function useToast() {
  const toastHelpers = React.useContext(ToastContext);
  return toastHelpers;
}
Enter fullscreen mode Exit fullscreen mode

We don’t need to import ToastContext because this hook resides along with it in ToastProvider component. And now we can simply call it like this:

const { addToast } = useToast();
Enter fullscreen mode Exit fullscreen mode

Dismissing toasts with timeout

We can add toasts with addToast and now they need to be automatically dismissed. I think the right place for this is a Toast component, since it is aware of its own lifecycle and aware of ID sent to it as props.

We need to fire a setTimeout with a call to removeToast after delay. The best way we can do this is using useEffect hook.

Side note about useEffect: it will run passed callback function whenever one of dependencies changes.

So, we will use removeToast and id in dependencies list for this effect, since everything used inside the function should be passed as a dependency. We assume(and know) that id and removeToast function won't change, that means the effect will only be called upon first render. Let's see how it looks in code:

const Toast = ({ children, id }) => {
  const { removeToast } = useToast();

  useEffect(() => {
    const timer = setTimeout(() => {
      removeToast(id);
    }, 3000); // delay

    return () => {
      clearTimeout(timer);
    };
  }, [id, removeToast]);

  // ...render toast content as before...
}
Enter fullscreen mode Exit fullscreen mode

Note the clean up function in useEffect: we need to clean up timer, so that it won't cause errors in case of unexpected removal of component.

That’s it! Now it works as expected. Feel free to play with the demo in CodeSandbox.

If you want to go further and practice more you can try enhancing it by adding some more customisation. For example by configuring delay, render position, styling and more. Most likely ToastProvider is the best place for that, since it is exposed to consumer and renders all other components.

If you liked the post check out my upcoming course 30-day-React. There you can practice more alike examples in a video format. It is still in progress, therefore I will give away the course for free when it is released to first 100 people who signs up now.

Top comments (0)