Unable to use a hook in a component - reactjs

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

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

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.
If you have imported React from "react" inside the scope of your component.
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.

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>

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

Related

Reducer state update causing a router wrapped in HOC to rerender in a loop

I found that the issue is stemming from a Higher Order Component that wraps around a react-router-dom hook.
This Higher Order Component is imported from #auth0/auth0-react and is a requirement in our project to handle logging out with redirect.
However, even just a basic HOC, the issue is persisting.
in my App.js file, I have a react-redux provider. And inside the provider I have a ProtectLayout component.
ProtectLayout checks for an error reducer, and if the error property in the reducer has a value, it sets a toast message, as seen below.
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
import Loadable from "react-loadable";
import { Switch } from "react-router-dom";
import PageLoader from "../loader/PageLoader";
import { useToast } from "../toast/ToastContext";
import { selectError } from "../../store/reducers/error/error.slice";
import ProtectedRoute from "../routes/ProtectedRoute";
const JobsPage = Loadable({
loader: () => import("../../screens/jobs/JobsPage"),
loading: () => <PageLoader loadingText="Getting your jobs..." />
});
const ProtectedLayout = () => {
const { openToast } = useToast();
const { error } = useSelector(selectError);
const getErrorDetails = async () => {
if (error) {
if (error?.title || error?.message)
return { title: error?.title, message: error?.message };
return {
title: "Error",
message: `Something went wrong. We couldn't complete this request`
};
}
return null;
};
useEffect(() => {
let isMounted = true;
getErrorDetails().then(
(e) =>
isMounted &&
(e?.title || e?.message) &&
openToast({ type: "error", title: e?.title, message: e?.message })
);
return () => {
isMounted = false;
};
}, [error]);
return (
<Switch>
<ProtectedRoute exact path="/" component={JobsPage} />
</Switch>
);
};
export default ProtectedLayout;
ProtectLayout returns another component ProtectedRoute. ProtectedRoute renders a react-router-dom Route component, which the component prop on the Route in the component prop passed into ProtectedRoute but wrapped in a Higher Order Component. In my actual application, as aforementioned, this is the withAuthenticationRequired HOC from #auth0/auth0-react which checks if an auth0 user is logged in, otherwise it logs the user out and redirects to the correct URL.
import React from "react";
import { Route } from "react-router-dom";
const withAuthenticationRequired = (Component, options) => {
return function WithAuthenticationRequired(props) {
return <Component {...props} />;
};
};
const ProtectedRoute = ({ component, ...args }) => {
return <Route component={withAuthenticationRequired(component)} {...args} />;
};
export default ProtectedRoute;
However, in one of the Route components, JobsPage the error reducer state is updated on mount, so what happens is the state gets updated, the ProtectedLayout re-renders, which then re-renders ProtectedRoute, which then re-renders JobPage which triggers the useEffect again, which updates the state, so you end up in an infinite loop.
import React, { useEffect } from "react";
import { useDispatch } from "react-redux";
import { getGlobalError } from "../../store/reducers/error/error.thunk";
const JobsPage = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getGlobalError(new Error("test")));
}, []);
return (
<div>
JOBS PAGE
</div>
);
};
export default JobsPage;
I have no idea how to prevent this rendering loop?
Really all I want to do, is that when there is an error thrown in a thunk action, it catches the error and updates the error reducer state. That will then trigger a toast message, using the useToast hook. Perhaps there is a better way around this, that what I currently have setup?
I have a CodeSandbox below to recreate this issue. If you click on the text you can see the re-renders occur, if you comment out the useEffect hook, it will basically crash the sandbox, so might be best to only uncomment when you think you have resolved the issue.
Any help would be greatly appreciated!

How can i use react-toastify from hook?

I found useToast and useToastContainer, but documentation is absent and i don't understand how tu use these hooks. Can anyone provide some info about these hooks?
The toasts inherit ToastContainer’s props. Props defined on toast supersede ToastContainer’s props.
There are two ways you can use toasts in your application:
1. Define ToastContainer inside the component
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
const App = () => {
notify = () => toast("Wow so easy !");
return (
<div>
<button onClick={notify}>Notify !</button>
// You can add <ToastContainer /> in root component as well.
<ToastContainer />
</div>
);
}
2. Call toast.configure() once in your app. At the root of your app is the best place.
The library will mount a ToastContainer for you if none is mounted.
import { toast } from "react-toastify";
import 'react-toastify/dist/ReactToastify.css';
// Call it once in your app. At the root of your app is the best place
toast.configure()
const App = () => {
notify = () => toast("Wow so easy !");
return (
<button onClick={notify}>Notify !</button>
);
}
You can use either of them. I prefer the 2nd method because you only need to define toast.configure() which is quite clean way to add it.
You can add configuration as per your need like below:
toast.configure({
autoClose: 8000,
draggable: false,
//etc you get the idea
});
EDIT
If you want to use toast hooks, then you must wrap your app with the ToastProvider to have access to its context elsewhere within your app.
import { ToastProvider, useToasts } from 'react-toast-notifications'
const FormWithToasts = () => {
const { addToast } = useToasts()
const onSubmit = async value => {
const { error } = await dataPersistenceLayer(value)
if (error) {
addToast(error.message, { appearance: 'error' })
} else {
addToast('Saved Successfully', { appearance: 'success' })
}
}
return <form onSubmit={onSubmit}>...</form>
}
const App = () => (
<ToastProvider>
<FormWithToasts />
</ToastProvider>
)

Define a functional component inside storybook preview

I have a custom modal component as functional component and in typescript. This modal component exposes api's through context providers and to access them, I'm using useContext hook.
const { openModal, closeModal } = useContext(ModalContext);
Example code on how I use this api's:
const TestComponent = () => {
const { openModal, closeModal } = useContext(ModalContext);
const modalProps = {}; //define some props
const open = () => {
openModal({...modalProps});
}
return (
<div>
<Button onClick={open}>Open Modal</Button>
</div>
)
}
And I wrap the component inside my ModalManager
<ModalManager>
<TestComponent />
</ModalManager>
This example works absolutely fine in my Modal.stories.tsx
Problem:
But this doesn't work inside my Modal.mdx. It says I cannot access react hooks outside functional component. So, I need to define a TestComponent like component to access my modal api's from context. How to define it and where to define it so that below code for preview works?
import {
Props, Preview, Meta
} from '#storybook/addon-docs/blocks';
<Meta title='Modal' />
<Preview
isExpanded
mdxSource={`
/* source of the component like in stories.tsx */
`}
>
<ModalManager><TestComponent /></ModalManager>
</Preview>
I'm not sure if this is a hack or the only way. I created the TestComponent in different tsx file and then imported it in mdx. It worked.
You may have a utility HOC to render it inside a MDX file as below
HOCComp.tsx in some Utils folder
import React, { FunctionComponent, PropsWithChildren } from 'react';
export interface HOCCompProps {
render(): React.ReactElement;
}
const HOCComp: FunctionComponent<HOCCompProps> = (props: PropsWithChildren<HOCCompProps>) => {
const { render } = props;
return render();
};
export default HOCComp;
Inside MDX File
import HOCComp from './HOC';
<HOCComp render={()=> {
function HOCImpl(){
const [count,setCount] = React.useState(180);
React.useEffect(() => {
const intId = setInterval(() => {
const newCount = count+1;
setCount(newCount);
},1000)
return () => {
clearInterval(intId);
}
})
return <Text>{count}</Text>
}
return <HOCImpl />
}}
/>

On click returns null instead of an object

It's really basic I guess. I'm trying to add onClick callback to my script & I believe I'm missing a value that would be responsible for finding the actual item.
Main script
import React from 'react';
import { CSVLink } from 'react-csv';
import { data } from 'constants/data';
import GetAppIcon from '#material-ui/icons/GetApp';
import PropTypes from 'prop-types';
const handleClick = (callback) => {
callback(callback);
};
const DownloadData = (props) => {
const { callback } = props;
return (
<>
<CSVLink
data={data}
onClick={() => handleClick(callback)}
>
<GetAppIcon />
</CSVLink>
</>
);
};
DownloadData.propTypes = {
callback: PropTypes.func.isRequired,
};
export default DownloadData;
Storybook code
import React from 'react';
import DownloadData from 'common/components/DownloadData';
import { data } from 'constants/data';
import { action } from '#storybook/addon-actions';
export default {
title: 'DownloadData',
component: DownloadData,
};
export const download = () => (
<DownloadData
data={data}
callback={action('icon-clicked')}
/>
);
So right now with this code on click in the storybook I'd get null and I'm looking for an object.
One of the potential issues I can see is that your handleClick function is stored as it is in-memory, when you import the component. That means you're keeping reference of something that doesn't exists and expects to use it when rendering the component with the callback prop.
Each instance of a component should have its own function. To fix it, move the function declaration inside the component. Like this:
const Foo = ({ callback }) => {
// handleClick needs to be inside here
const handleClick = callback => {
console.log("clicked");
callback(callback);
};
return <div onClick={() => handleClick(callback)}>Click me!</div>;
};
Check this example.
If this doesn't fix your problem, then there is something wrong with how you're implementing Storybook. Like a missing context.

How would I use React Hooks to replace my withAuth() HOC?

I've been spending a bunch of time reading up on React Hooks, and while the functionality seems more intuitive, readable, and concise than using classes with local state and lifecycle methods, I keep reading references to Hooks being a replacement for HOCs.
The primary HOC I have used in React apps is withAuth -- basically a function that checks to see if the currentUser (stored in Redux state) is authenticated, and if so, to render the wrapped component.
Here is an implementation of this:
import React, { Component } from "react";
import { connect } from "react-redux";
export default function withAuth(ComponentToBeRendered) {
class Authenticate extends Component {
componentWillMount() {
if (this.props.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
componentWillUpdate(nextProps) {
if (nextProps.isAuthenticated === false) {
this.props.history.push("/signin");
}
}
render() {
return <ComponentToBeRendered {...this.props} />;
}
}
function mapStateToProps(state) {
return { isAuthenticated: state.currentUser.isAuthenticated };
}
return connect(mapStateToProps)(Authenticate);
}
What I can't see is how I can replace this HOC with hooks, especially since hooks don't run until after the render method is called. That means I would not be able to use a hook on what would have formerly been ProtectedComponent (wrapped with withAuth) to determine whether to render it or not since it would already be rendered.
What is the new fancy hook way to handle this type of scenario?
render()
We can reframe the question of 'to render or not to render' a tiny bit. The render method will always be called before either hook-based callbacks or lifecycle methods. This holds except for some soon-to-be deprecated lifecycle methods.
So instead, your render method (or functional component) has to handle all its possible states, including states that require nothing be rendered. Either that, or the job of rendering nothing can be lifted up to a parent component. It's the difference between:
const Child = (props) => props.yes && <div>Hi</div>;
// ...
<Parent>
<Child yes={props.childYes} />
</Parent>
and
const Child = (props) => <div>Hi</div>;
// ...
<Parent>
{props.childYes && <Child />}
</Parent>
Deciding which one of these to use is situational.
Hooks
There are ways of using hooks to solve the same problems the HOCs do. I'd start with what the HOC offers; a way of accessing user data on the application state, and redirecting to /signin when the data signifies an invalid session. We can provide both of those things with hooks.
import { useSelector } from "react-redux";
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const MySecurePage = props => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated && <MyPage {...props} />;
};
A couple of things happening in the example above. We're using the useSelector hook from react-redux to access the the state just as we were previously doing using connect, only with much less code.
We're also using the value we get from useSelector to conditionally fire a side effect with the useEffect hook. By default the callback we pass to useEffect is called after each render. But here we also pass an array of the dependencies, which tells React we only want the effect to fire when a dependency changes (in addition to the first render, which always fires the effect). Thus we will be redirected when isAuthenticated starts out false, or becomes false.
While this example used a component definition, this works as a custom hook as well:
const mapState = state => ({
isAuthenticated: state.currentUser.isAuthenticated
});
const useAuth = () => {
const { isAuthenticated } = useSelector(mapState);
useEffect(
() => {
if (!isAuthenticated) {
history.push("/signin");
}
},
[isAuthenticated]
);
return isAuthenticated;
};
const MySecurePage = (props) => {
return useAuth() && <MyPage {...props} />;
};
One last thing - you might wonder about doing something like this:
const AuthWrapper = (props) => useAuth() && props.children;
in order to be able to do things like this:
<AuthWrapper>
<Sensitive />
<View />
<Elements />
</AuthWrapper>
You may well decide this last example is the approach for you, but I would read this before deciding.
Building on the answer provided by backtick, this chunk of code should do what you're looking for:
import React, { useEffect } from "react";
import { useSelector } from "react-redux";
const withAuth = (ComponentToBeRendered) => {
const mapState = (state) => ({
isAuthenticated: state.currentUser.isAuthenticated,
});
const Authenticate = (props) => {
const { isAuthenticated } = useSelector(mapState);
useEffect(() => {
if (!isAuthenticated) {
props.history.push("/signin");
}
}, [isAuthenticated]);
return isAuthenticated && <ComponentToBeRendered {...props} />;
};
return Authenticate;
};
export default withAuth;
You could render this in a container using React-Router-DOM as such:
import withAuth from "../hocs/withAuth"
import Component from "../components/Component"
// ...
<Route path='...' component={withAuth(Component)} />

Resources