Sri
Sri

Reputation: 2328

Unable to use a hook in a component

I am trying to use a hook but I get the following error when using the useSnackbar hook from notistack.

Error: Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
1. You might have mismatching versions of React and the renderer (such as React DOM)
2. You might be breaking the Rules of Hooks
3. You might have more than one copy of React in the same app

My App.js

 <SnackbarProvider
      anchorOrigin={{
        vertical: 'top',
        horizontal: 'center',
      }}
    >
      <App />
 </SnackbarProvider>

My SnackBar.js

const SnackBar = (message, severity) => {
  const { enqueueSnackbar, closeSnackbar } = useSnackbar()
  const action = key => (
    <>
      <Button
        onClick={() => {
          closeSnackbar(key)
        }}
      >
        Dismiss
      </Button>
    </>
  )

  enqueueSnackbar(message, {
    variant: severity,
    autoHideDuration: severity === 'error' ? null : 5000,
    action,
    preventDuplicate: true,
    TransitionComponent: Fade,
  })
}

My demo.js contains this function

const Demo = props => {
    const showSnackBar = (message, severity) => {
      SnackBar(message, severity)
    }
}

If I were to call the hook in demo.js and pass it in as an argument like the following it works. What is the difference? Why can't I use the useSnackbar() hook in snackbar.js?

const Demo = props => {
    const showSnackBar = (message, severity) => {
      SnackBar(enqueueSnackbar, closeSnackbar, message, severity)
    }
}

Upvotes: 2

Views: 5626

Answers (4)

Carlos
Carlos

Reputation: 1

UPDATE: The reason you can't call the useSnackbar() in snackbar.js is because snackbar.js is not a functional component. The mighty rules of hooks (https://reactjs.org/docs/hooks-rules.html) state that you can only call hooks from: 1) the body of functional components 2) other custom hooks. I recommend refactoring as you have done to call the hook first in demo.js and passing the response object (along with say the enqueueSnackbar function) to any other function afterwards.

PREVIOUS RESPONSE:

Prabin's solution feels a bit hacky but I can't think of a better one to allow for super easy to use global snackbars.

For anyone getting "TypeError: Cannot destructure property 'enqueueSnackbar' of 'Object(...)(...)' as it is undefined"

This was happening to me because I was using useSnackbar() inside my main app.js (or router) component, which, incidentally, is the same one where the component is initialized. You cannot consume a context provider in the same component that declares it, it has to be a child element. So, I created an empty component called Snackbar which handles saving the enqueueSnackbar and closeSnackbar to the global class (SnackbarUtils.js in the example answer).

Upvotes: 0

Prabin Karki
Prabin Karki

Reputation: 11

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

Upvotes: 1

Yatrix
Yatrix

Reputation: 13775

Change

const SnackBar = (message, severity) => { }

to

const SnackBar = ({ message, severity }) => { }

and you have to return some mark-up as well,

return <div>Some stuff</div>

Upvotes: 0

Kevin Moe Myint Myat
Kevin Moe Myint Myat

Reputation: 2165

Hooks are for React components which are JSX elements coated in a syntactic sugar.

Currently, you are using useSnackbar() hook inside SnackBar.js

In order to work, SnackBar.js must be a React component.

Things to check.

  1. If you have imported React from "react" inside the scope of your component.
  2. If you have return a JSX tag for the component to render.

For your case,

  • Your SnackBar.js is not a component since it doesn't return anything.
  • Your demo.js works because it is a component and it already called the hook and then pass the result down to child function.

Upvotes: 0

Related Questions