Notistack: Support for displaying snackbar outside of component

Created on 28 Nov 2018  Β·  28Comments  Β·  Source: iamhosseindhv/notistack

Expected Behavior

Current Behavior

Steps to Reproduce


Link:

1.
2.
3.
4.

Context

Your Environment

| Tech | Version |
|--------------|---------|
| Notistack | v1.?.? |
| React | |
| Browser | |
| etc. | |

Most helpful comment

Hey guys,

I did not like that ReactDOM.render() would be called every time.

Here is my take on it:

import { useSnackbar, VariantType, WithSnackbarProps } from 'notistack'
import React from 'react'

interface IProps {
  setUseSnackbarRef: (showSnackbar: WithSnackbarProps) => void
}

const InnerSnackbarUtilsConfigurator: React.FC<IProps> = (props: IProps) => {
  props.setUseSnackbarRef(useSnackbar())
  return null
}

let useSnackbarRef: WithSnackbarProps
const setUseSnackbarRef = (useSnackbarRefProp: WithSnackbarProps) => {
  useSnackbarRef = useSnackbarRefProp
}

export const SnackbarUtilsConfigurator = () => {
  return <InnerSnackbarUtilsConfigurator setUseSnackbarRef={setUseSnackbarRef} />
}

export default {
  success(msg: string) {
    this.toast(msg, 'success')
  },
  warning(msg: string) {
    this.toast(msg, 'warning')
  },
  info(msg: string) {
    this.toast(msg, 'info')
  },
  error(msg: string) {
    this.toast(msg, 'error')
  },
  toast(msg: string, variant: VariantType = 'default') {
    useSnackbarRef.enqueueSnackbar(msg, { variant })
  }
}

To initialize:

<SnackbarProvider maxSnack={3} anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}>
    <SnackbarUtilsConfigurator />
    ...
</SnackbarProvider>

And then, anywhere in code:

import SnackbarUtils from 'src/utils/SnackbarUtils'
SnackbarUtils.success('Success πŸŽ‰')

Also, with this implementation, you can expose anything the Hook or HOC was providing easily.

Cheers πŸ₯‚

All 28 comments

I need more detail about what you want to achieve before I can help you @nguyenvanphuc2203

image
i have a function export fetch api, and i want to use notistack replace alert

where do you call the function fetchApi?

(on a side note: token is always undefined in line 19. variable token is only accessible from within the first if statement. to access it outside of if statement do something like the following):

let token = null;
if (...) {
    token = //get from localStorage
} else {
   // ...
}
// ...
const authorization = token ? {} : {'Authorization': `Bearer ${token}`}
// ...

update: I was not paying attention that token is a var. var is function scoped therefore no problem with your code.

oh! thank for feedback,
i use fetch api function in some component

please provide the code of that component

image
a example,

image
butut i want to use notistack inside function common fetch api to show error

enqueueSnackbar can only be accessed via props and props are only available in your component's code. what I recommend is to move the logic (things like checking for status, setting token in localStorage, etc) to fetchApi or another function, and just return promise, resolve on success and reject on failure. That way you can display snackbars and also have a clean, easy-to-follow code. Your result would look something like this:

fetchApi(url, method, payload)
    .then(() => {
           // ... enqueue snackbar
           // ... redirect to /dashboard page
    })
    .catch((err) => this.props.enqueueSnackbar(err, { variant: 'error' } ))

check this out for more info.

thanks for your help
<3

I have the same use case. It's not possible to tie the notification to any component (it appears in a socket.io event handler). I resolved this by creating a second instance of SnackbarProvider mounted elsewhere in the DOM, as shown below.

<div id='snackbarhelper'></div>

and then this should work:

import React from 'react';
import ReactDOM from 'react-dom';
import { SnackbarProvider, withSnackbar } from 'notistack';

export default {
  success: function (msg) {
    this.toast(msg, 'success');
  },
  toast: function (msg, variant) {
    const Display = withSnackbar(({ message, enqueueSnackbar }) => {
      enqueueSnackbar(message, { variant });
      return null;
    });
    const mountPoint = document.getElementById('snackbarhelper');
    ReactDOM.render(
      <SnackbarProvider maxSnack={3} anchorOrigin={{horizontal: 'left', vertical: 'bottom'}}>
        <Display message={msg} variant={variant} />
      </SnackbarProvider>,
      mountPoint)
  }
}

Then use like so:

import toast from '....'

toast.success("It works!")

As a sidenote/question: I first tried this with const mountPoint = document.createElement('div') and then mount it to an unattached DOMNode. This technique is from here. However, this didn't work - is this an issue with notistack or material-ui?

Thanks for sharing your solution @godmar.

I found @godmar's solution useful, since I wanted to do some logging from inside window.setInterval callbacks.

@iamhosseindhv maybe it would be worth mentioning this approach in the readme?

ps: instead of requiring snackbarhelper, we can of course just attach the node ourselves to the DOM.

Updated code:

import React from 'react';
import ReactDOM from 'react-dom';
import { SnackbarProvider, useSnackbar } from 'notistack';

// add a <div> child to body under which to mount the snackbars
const mountPoint = document.createElement('div');
document.body.appendChild(mountPoint);

export default {
  success: function(msg) {
    this.toast(msg, 'success');
  },
  warning: function(msg) {
    this.toast(msg, 'warning');
  },
  info: function(msg) {
    this.toast(msg, 'info');
  },
  error: function(msg) {
    this.toast(msg, 'error');
  },
  toast: function(msg, variant = 'default') {
    const ShowSnackbar = ({ message }) => {
      const { enqueueSnackbar } = useSnackbar();
      enqueueSnackbar(message, { variant });
      return null;
    };
    ReactDOM.render(
      // see https://github.com/iamhosseindhv/notistack#snackbarprovider
      <SnackbarProvider
        maxSnack={3}
        anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
      >
        <ShowSnackbar message={msg} variant={variant} />
      </SnackbarProvider>,
      mountPoint
    );
  }
};

Hey guys,

I did not like that ReactDOM.render() would be called every time.

Here is my take on it:

import { useSnackbar, VariantType, WithSnackbarProps } from 'notistack'
import React from 'react'

interface IProps {
  setUseSnackbarRef: (showSnackbar: WithSnackbarProps) => void
}

const InnerSnackbarUtilsConfigurator: React.FC<IProps> = (props: IProps) => {
  props.setUseSnackbarRef(useSnackbar())
  return null
}

let useSnackbarRef: WithSnackbarProps
const setUseSnackbarRef = (useSnackbarRefProp: WithSnackbarProps) => {
  useSnackbarRef = useSnackbarRefProp
}

export const SnackbarUtilsConfigurator = () => {
  return <InnerSnackbarUtilsConfigurator setUseSnackbarRef={setUseSnackbarRef} />
}

export default {
  success(msg: string) {
    this.toast(msg, 'success')
  },
  warning(msg: string) {
    this.toast(msg, 'warning')
  },
  info(msg: string) {
    this.toast(msg, 'info')
  },
  error(msg: string) {
    this.toast(msg, 'error')
  },
  toast(msg: string, variant: VariantType = 'default') {
    useSnackbarRef.enqueueSnackbar(msg, { variant })
  }
}

To initialize:

<SnackbarProvider maxSnack={3} anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}>
    <SnackbarUtilsConfigurator />
    ...
</SnackbarProvider>

And then, anywhere in code:

import SnackbarUtils from 'src/utils/SnackbarUtils'
SnackbarUtils.success('Success πŸŽ‰')

Also, with this implementation, you can expose anything the Hook or HOC was providing easily.

Cheers πŸ₯‚

@joelbourbon I get
'InnerSnackbarUtilsConfigurator' refers to a value, but is being used as a type here.
when trying to add that code to a ts file

@mihai93 this line is a TSX line. You must have a *.tsx file.

Working Code Sandbox here.

Replied to a comment

Thanks a lot! 😍

I just updated a little bit your code:

export default {
  success(msg: string, options: OptionsObject = {}) {
    this.toast(msg, { ...options, variant: 'success' })
  },
  warning(msg: string, options: OptionsObject = {}) {
    this.toast(msg, { ...options, variant: 'warning' })
  },
  info(msg: string, options: OptionsObject = {}) {
    this.toast(msg, { ...options, variant: 'info' })
  },
  error(msg: string, options: OptionsObject = {}) {
    this.toast(msg, { ...options, variant: 'error' })
  },
  toast(msg: string, options: OptionsObject = {}) {
    useSnackbarRef.enqueueSnackbar(msg, options)
  }
}

Reopening so people know support for it is going to be provided natively from notistack.

Replied to a comment

Unfortunately, maxSnack prop does not work when I fire the snackbars using this :/

EDIT: Ignore this comment, I was executing closeSnackbar() somewhere… my bad, I need to take a break.

@iamhosseindhv are you working on this yet?

a colleague and I came down with a simpler configurator:

let snackbarRef: WithSnackbarProps;

export const SnackbarUtilsConfigurator: React.FC = () => {
  snackbarRef = useSnackbar();
  return null;
};

πŸ‘€

Thanks for sharing your solution @godmar.

Hey guys,

I did not like that ReactDOM.render() would be called every time.

Here is my take on it:

import { useSnackbar, VariantType, WithSnackbarProps } from 'notistack'
import React from 'react'

interface IProps {
  setUseSnackbarRef: (showSnackbar: WithSnackbarProps) => void
}

const InnerSnackbarUtilsConfigurator: React.FC<IProps> = (props: IProps) => {
  props.setUseSnackbarRef(useSnackbar())
  return null
}

let useSnackbarRef: WithSnackbarProps
const setUseSnackbarRef = (useSnackbarRefProp: WithSnackbarProps) => {
  useSnackbarRef = useSnackbarRefProp
}

export const SnackbarUtilsConfigurator = () => {
  return <InnerSnackbarUtilsConfigurator setUseSnackbarRef={setUseSnackbarRef} />
}

export default {
  success(msg: string) {
    this.toast(msg, 'success')
  },
  warning(msg: string) {
    this.toast(msg, 'warning')
  },
  info(msg: string) {
    this.toast(msg, 'info')
  },
  error(msg: string) {
    this.toast(msg, 'error')
  },
  toast(msg: string, variant: VariantType = 'default') {
    useSnackbarRef.enqueueSnackbar(msg, { variant })
  }
}

To initialize:

<SnackbarProvider maxSnack={3} anchorOrigin={{ horizontal: 'center', vertical: 'bottom' }}>
    <SnackbarUtilsConfigurator />
    ...
</SnackbarProvider>

And then, anywhere in code:

import SnackbarUtils from 'src/utils/SnackbarUtils'
SnackbarUtils.success('Success πŸŽ‰')

Also, with this implementation, you can expose anything the Hook or HOC was providing easily.

Cheers πŸ₯‚

Could you please provide the same solution without TS?

@ahmsalah
JS version is here.

The Easy way
Store the enqueueSnackbar & closeSnackbar in the some class variable at the time of startup of the application, And use anywhere in your application.
Follow the steps down below,

1.Store Both enqueueSnackbar & closeSnackbar to class variable inside the Routes.js file.

import React, { Component, useEffect, useState } from 'react';
import {Switch,Route, Redirect, useLocation} from 'react-router-dom';
import AppLayout from '../components/common/AppLayout';
import PrivateRoute from '../components/common/PrivateRoute';
import DashboardRoutes from './DashboardRoutes';
import AuthRoutes from './AuthRoutes';
import Auth from '../services/https/Auth';
import store from '../store';
import { setCurrentUser } from '../store/user/action';
import MySpinner from '../components/common/MySpinner';
import { SnackbarProvider, useSnackbar } from "notistack";
import SnackbarUtils from '../utils/SnackbarUtils';

const Routes = () => {
    const location = useLocation()
    const [authLoading,setAuthLoading] = useState(true)

    //1. UseHooks to get enqueueSnackbar, closeSnackbar
    const { enqueueSnackbar, closeSnackbar } = useSnackbar();

    useEffect(()=>{

    //2. Store both  enqueueSnackbar & closeSnackbar to class variables
        SnackbarUtils.setSnackBar(enqueueSnackbar,closeSnackbar)
        const currentUser = Auth.getCurrentUser()
        store.dispatch(setCurrentUser(currentUser))
        setAuthLoading(false)
    },[])
    if(authLoading){
        return(
            <MySpinner title="Authenticating..."/>
        )
    }
    return ( 
        <AppLayout 
        noLayout={location.pathname=="/auth/login"||location.pathname=="/auth/register"}
        >
            <div>
                <Switch>
                    <Redirect from="/" to="/auth" exact/>
                    <PrivateRoute redirectWithAuthCheck={true}  path = "/auth" component={AuthRoutes}/>
                    <PrivateRoute path = "/dashboard" component={DashboardRoutes}/>
                    <Redirect  to="/auth"/>
                </Switch>
            </div>
        </AppLayout>
     );
}

export default Routes;

2. This is how SnackbarUtils.js file looks like

class SnackbarUtils {
  #snackBar = {
    enqueueSnackbar: ()=>{},
    closeSnackbar: () => {},
  };

  setSnackBar(enqueueSnackbar, closeSnackbar) {
    this.#snackBar.enqueueSnackbar = enqueueSnackbar;
    this.#snackBar.closeSnackbar = closeSnackbar;
  }

  success(msg, options = {}) {
    return this.toast(msg, { ...options, variant: "success" });
  }
  warning(msg, options = {}) {
    return this.toast(msg, { ...options, variant: "warning" });
  }
  info(msg, options = {}) {
    return this.toast(msg, { ...options, variant: "info" });
  }

  error(msg, options = {}) {
    return this.toast(msg, { ...options, variant: "error" });
  }
  toast(msg, options = {}) {
    const finalOptions = {
      variant: "default",
      ...options,
    };
    return this.#snackBar.enqueueSnackbar(msg, { ...finalOptions });
  }
  closeSnackbar(key) {
    this.#snackBar.closeSnackbar(key);
  }
}

export default new SnackbarUtils();

3.Now just import the SnackbarUtils and use snackbar anywhere in your application as follows.

<button onClick={()=>{
           SnackbarUtils.success("Hello")
        }}>Show</button>

You can use snackbar in non react component file also
happy coding πŸ‘

I allowed myself to present a JS version of the TSX solution by @joelbourbon whom I thank for the good job by the way πŸ‘
First, declare a file called SnackBarUtils.js inside a utils folder or do it your way and set the following code inside

import { useSnackbar } from 'notistack'
import React from 'react'



const InnerSnackbarUtilsConfigurator = (props) => {
  props.setUseSnackbarRef(useSnackbar())
  return null;
}

let useSnackbarRef;
const setUseSnackbarRef = (useSnackbarRefProp) => {
  useSnackbarRef = useSnackbarRefProp
}

export const SnackbarUtilsConfigurator = () => {
  return <InnerSnackbarUtilsConfigurator setUseSnackbarRef={setUseSnackbarRef} />
}

export const snackActions = {
  success(msg) {
    this.toast(msg, 'success')
  },
  warning(msg) {
    this.toast(msg, 'warning')
  },
  info(msg) {
    this.toast(msg, 'info')
  },
  error(msg) {
    this.toast(msg, 'error')
  },
  toast(msg, variant = 'default') {
    useSnackbarRef.enqueueSnackbar(msg, { variant })
  }
}

Import SnackbarUtilsConfigurator inside your entry component ( by default in React apps -> src/index.js ) like in the code below

import { SnackbarUtilsConfigurator } from "./utils/SnackbarUtils";


ReactDOM.render(
  <StoreProvider store={store}>
    <SnackbarProvider maxSnack={2}>
      <SnackbarUtilsConfigurator />
      <App />
    </SnackbarProvider>
  </StoreProvider>,
  document.getElementById("root")
);

Finally, import snackActions from wherever you have your SnackBarUtils.js file.
In the following example, I have a model for my easy-peasy store ( it's a better and cleaner implementation of a redux store imo ), I fetch a list of projects and once it's done loading I show a success message. Of course this is just a basic case, you can customize your messages by modifying the SnackBarUtils file and calling snackActions with different parameters

import { snackActions } from '../utils/SnackbarUtils';

getProjects: thunk(async (actions, payload) => {
    actions.setSidebarIsLoadingProjects(true);
    const projects = await getProjects();
    if (!projects) {
      actions.setProjects([]);
      actions.setSidebarIsLoadingProjects(false);
      snackActions.error('Failed to fetch projects');
    } else {
      actions.setProjects(projects);
      actions.setSidebarIsLoadingProjects(false);
      snackActions.success('Succsefully fetched projects');
    }
  })

Just implemented TSX my variant of solutions presented above. Decided to share it here, because:

  • it allows to call globallyAvailableNotifications.enqueueErrorNotification(…) anywhere in the code without worrying of undefined globallyStoredSnackbar
  • it defines less components (I don't understand reasoning behind SnackbarUtilsConfigurator + InnerSnackbarUtilsConfigurator)
  • it is more explicit about preferred usage (short name useNotifications) and hacky-but-diable usage (lengthy and more explicit in name globallyAvailableNotifications)
  • it exposes own API, which allows to narrow down how do we agreed to use given library in our project (eg. if we want to show notifications only for errors)
  • it gathers all "notistack" behind notistack "namespace" (due to * as notistack import)
import React from "react";
import * as notistack from "notistack";
import { makeStyles as makeMuiStyles } from "@material-ui/core/styles";

export interface Notifications {
  enqueueErrorNotification: (errorMessage: string) => void;
}

export const useNotifications = (): Notifications => {
  const snackbar = notistack.useSnackbar();

  return {
    enqueueErrorNotification: (errorMessage) => {
      snackbar.enqueueSnackbar(errorMessage, {
        variant: "error",
      });
    },
  };
};

// Notifications available outside components, based on https://github.com/iamhosseindhv/notistack/issues/30#issuecomment-542863653
let globallyStoredSnackbar: notistack.ProviderContext | undefined;
export const globallyAvailableNotifications: Notifications = {
  enqueueErrorNotification: (errorMessage) => {
    globallyStoredSnackbar?.enqueueSnackbar(errorMessage, {
      variant: "error",
    });
  },
};

export const NotificationsProvider: React.FC = ({ children }) => {
  const classes = useStyles();

  const GloballyStoredSnackbarProvider: React.FC = React.useMemo(
    () => () => {
      globallyStoredSnackbar = notistack.useSnackbar();
      return null;
    },
    [],
  );

  return (
    <notistack.SnackbarProvider
      maxSnack={3}
      autoHideDuration={5000}
      hideIconVariant
      classes={{
        variantError: classes.error,
      }}
    >
      <GloballyStoredSnackbarProvider />
      {children}
    </notistack.SnackbarProvider>
  );
};

const useStyles = makeMuiStyles(() => ({
  error: {
    backgroundColor: `rgb(255, 0, 0) !important`,
  },
}));

I allowed myself to present a JS version of the TSX solution by @joelbourbon whom I thank for the good job by the way +1
First, declare a file called SnackBarUtils.js inside a utils folder or do it your way and set the following code inside

import { useSnackbar } from 'notistack'
import React from 'react'



const InnerSnackbarUtilsConfigurator = (props) => {
  props.setUseSnackbarRef(useSnackbar())
  return null;
}

let useSnackbarRef;
const setUseSnackbarRef = (useSnackbarRefProp) => {
  useSnackbarRef = useSnackbarRefProp
}

export const SnackbarUtilsConfigurator = () => {
  return <InnerSnackbarUtilsConfigurator setUseSnackbarRef={setUseSnackbarRef} />
}

export const snackActions = {
  success(msg) {
    this.toast(msg, 'success')
  },
  warning(msg) {
    this.toast(msg, 'warning')
  },
  info(msg) {
    this.toast(msg, 'info')
  },
  error(msg) {
    this.toast(msg, 'error')
  },
  toast(msg, variant = 'default') {
    useSnackbarRef.enqueueSnackbar(msg, { variant })
  }
}

Import SnackbarUtilsConfigurator inside your entry component ( by default in React apps -> src/index.js ) like in the code below

import { SnackbarUtilsConfigurator } from "./utils/SnackbarUtils";


ReactDOM.render(
  <StoreProvider store={store}>
    <SnackbarProvider maxSnack={2}>
      <SnackbarUtilsConfigurator />
      <App />
    </SnackbarProvider>
  </StoreProvider>,
  document.getElementById("root")
);

Finally, import snackActions from wherever you have your SnackBarUtils.js file.
In the following example, I have a model for my easy-peasy store ( it's a better and cleaner implementation of a redux store imo ), I fetch a list of projects and once it's done loading I show a success message. Of course this is just a basic case, you can customize your messages by modifying the SnackBarUtils file and calling snackActions with different parameters

import { snackActions } from '../utils/SnackbarUtils';

getProjects: thunk(async (actions, payload) => {
    actions.setSidebarIsLoadingProjects(true);
    const projects = await getProjects();
    if (!projects) {
      actions.setProjects([]);
      actions.setSidebarIsLoadingProjects(false);
      snackActions.error('Failed to fetch projects');
    } else {
      actions.setProjects(projects);
      actions.setSidebarIsLoadingProjects(false);
      snackActions.success('Succsefully fetched projects');
    }
  })

@ethubert, thanks for sharing the code. I might have missing something but I think that we don't need to create component inside component like in SnackBarUtils.js.

It's enough to

let useSnackbarRef = null;

export const SnackbarUtilsConfigurator = () => {
  useSnackbarRef = useSnackbar();

  return null;
};

Was this page helpful?
0 / 5 - 0 ratings

Related issues

RastislavMirek picture RastislavMirek  Β·  8Comments

njetsy picture njetsy  Β·  6Comments

ksavery picture ksavery  Β·  3Comments

james-cordeiro picture james-cordeiro  Β·  6Comments

iamhosseindhv picture iamhosseindhv  Β·  8Comments