1

I am using PrimeReact's toast component, whose API looks like this:

function App() {
  const toast = useRef(null);

  useEffect(() => {
    toast.current.show({
      severity: 'info',
      detail: 'Hellope'
    });
  });

  return (
    <div className='App'>
      <Toast ref={toast} />
    </div>
  );
}

I would now like to call toast.current.show() from a non-React context. In particular, I have an http() utility function through which all HTTP calls are made. Whenever one fails, I would like to show a toast. What are clean/idiomatic ways to achieve this?

DanielM
  • 1,106
  • 3
  • 17
  • 27
  • Why isn't your `http()` utility function inside a custom hook? – Konrad Jan 03 '23 at 16:34
  • I guess I didn't see a reason to put it in one. At the moment, it's a generic Axios-like function that knows nothing about React. What would it mean to have it inside a custom hook and how would that help? – DanielM Jan 03 '23 at 16:40
  • There are many similar questions with a working answer. I'd suggest to look for promises and async/await or callbacks. – paddotk Jan 03 '23 at 16:46
  • *I guess I didn't see a reason to put it in one* - you can do it to access toasts for example – Konrad Jan 03 '23 at 16:49
  • @Konrad, I'm sorry, but I can't quite see how that idea would play out. – DanielM Jan 04 '23 at 08:33

4 Answers4

2

I would create a toast context that would allow showing toasts

toast-context.js

import "primereact/resources/themes/lara-light-indigo/theme.css";
import "primereact/resources/primereact.css";
import { Toast } from "primereact/toast";
import { createContext, useContext, useRef } from "react";

// create context
const ToastContext = createContext(undefined);

// wrap context provider to add functionality
export const ToastContextProvider = ({ children }) => {
  const toastRef = useRef(null);

  const showToast = (options) => {
    if (!toastRef.current) return;
    toastRef.current.show(options);
  };

  return (
    <ToastContext.Provider value={{ showToast }}>
      <Toast ref={toastRef} />
      <div>{children}</div>
    </ToastContext.Provider>
  );
};

export const useToastContext = () => {
  const context = useContext(ToastContext);

  if (!context) {
    throw new Error(
      "useToastContext have to be used within ToastContextProvider"
    );
  }

  return context;
};

index.js

import { StrictMode } from "react";
import { createRoot } from "react-dom/client";

import App from "./App";
import { ToastContextProvider } from "./toast-context";

const rootElement = document.getElementById("root");
const root = createRoot(rootElement);

root.render(
  <StrictMode>
    <ToastContextProvider>
      <App />
    </ToastContextProvider>
  </StrictMode>
);

App.js

import { useToastContext } from "./toast-context";

export default function App() {
  // use context to get the showToast function
  const { showToast } = useToastContext();
  const handleClick = () => {
    http(showToast);
  };

  return (
    <div className="App">
      <button onClick={handleClick}>show toast</button>
    </div>
  );
}

// pass showToast callback to your http function
function http(showToast) {
  showToast({
    severity: "success",
    summary: "Success Message",
    detail: "Order submitted"
  });
}

Codesanbox example: https://codesandbox.io/s/beautiful-cray-rzrfne?file=/src/App.js

Konrad
  • 21,590
  • 4
  • 28
  • 64
  • 1
    Oh, I see. I considered passing the toast reference as an argument as well, just without hooks, since I have flexibility to change the implementation of `http()`, i.e. `function http(toast) { ... }`. You see the problem here, though, right? `http()` is used in dozens of places, and I would like to avoid having to either write one `` with its reference in every place from which a request is made, or keeping a single `` in `` but having to propagate its reference all to all components that need it. – DanielM Jan 04 '23 at 09:06
1

Initialize the toast on the window object.

  useLayoutEffect(() => {
    window.PrimeToast = toast.current || {};
  }, []);

On your fetch or axios handler, use the above object on your error handler

   const fakeUrl = "https://api.afakeurl.com/hello";
   fetch(fakeUrl)
      .then((res) => res.data)
      .catch((err) => {
        console.error("error fetching request", err);
        if (window.PrimeToast) {
          window.PrimeToast.show({
            severity: "error",
            summary: "Error calling https",
            detail: "hello"
          });
        }
      });

Updated Sandbox

Reference:

  1. https://www.primefaces.org/primereact/toast/
Badal Saibo
  • 2,499
  • 11
  • 23
0

Here is one solution I have been experimenting with, although I have the impression it isn't very idiomatic. I suppose one could look at it as a "micro-frontend" responsible exclusively for showing toasts.

import ReactDOM from 'react-dom/client';
import { RefObject, useRef } from 'react';
import { Toast, ToastMessage } from 'primereact/toast';

class NotificationService {
  private toast?: RefObject<Toast>;

  constructor() {
    const toastAppRoot = document.createElement('div');
    document.body.append(toastAppRoot);

    const ToastApp = () => {
      this.toast = useRef<Toast>(null);
      return <Toast ref={this.toast} />;
    };

    ReactDOM.createRoot(toastAppRoot).render(<ToastApp />);
  }

  showToast(message: ToastMessage) {
    this.toast!.current!.show(message);
  }
}

export const notificationService = new NotificationService();

The simplicity of its usage is what's really nice of an approach like this. Import the service, call its method. It used to be that simple.

DanielM
  • 1,106
  • 3
  • 17
  • 27
  • Quite a unique approach. I wonder how each new instance of `NotificationService` would end up polluting the DOM nodes. Totally depends on the number of instance we have though. – Badal Saibo Jan 08 '23 at 14:30
  • Typically, a service like this would be a singleton. – DanielM Jan 09 '23 at 14:42
0

I've implemented a solution using events on the window object here: gist of the solution

It works by wrapping the components that need toasts in a ToastProvider where the Toast component is used and all the logic of toast display is coded. A useLayoutEffect adds a listener to "toast" events to window. Then, you can just emit this kind of event on window from anywhere in your app it will display a toast (even outside of a component).

The gist above contains code implementing a hook useToast and a toast object with same purpose functions/methods (show, showInfo, showSuccess, showWarn and showError).

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community May 22 '23 at 15:13