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

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

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!

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

Redux using functional component without React

I have a functional componet without React but that uses Redux like following:
export const isAuthenticated = () => ({user}) => {
console.log("user : ", user);
return true;
};
const mapStateToProps = (state) => {
return {
user: state.auth.userInfo
}
}
export default connect(mapStateToProps)(isAuthenticated as any)
And to use above function, I uses like:
{isAuthenticated() && (
<li className="nav-item">
<NavLink
className="nav-link"
activeStyle={{
color: "#1ebba3"
}}
to="/dashboard"
onClick={(e) => { if (this.menu.classList.contains("show")) { this.inputElement.click() } }}
>
Dashboard
</NavLink>
</li>
)}
It doesn't work. It just doesn't even get into that isAuthenticated function since I don't see any output for console.log("user : ", user);. It should output something like user: undefined, but it doesn't even output that.
If I change
export const isAuthenticated = () => ({user}) => {
to
export const isAuthenticated = ({user}) => {
then the problem is that I can't call it with isAuthenticated() and might be duplication between passed param from function call and retrived state from Redux.
How can I fix it if I want to keep using "isAuthenticated()" for calling that method, without passing any param, but let Redux pass user state to that function?
This can be solved with React's Hooks API. What you are aiming for is a custom hook that will internally use useSelector from react-redux. If you don't want to use functional components, you can always opt for Higher-Order Components (HOCs)
Code Samples
Custom Hook
import { useSelector } from 'react-redux';
export function useIsAuthenticated() {
return useSelector(state => !!state.auth.userInfo);
}
export function YourComponent(props) {
const isAuthenticated = useIsAuthenticate();
// Return your react sub-tree here based on `isAuthenticated`
// instead of `isAuthenticated()` like before.
}
Higher-Order Components
import { connect } from 'react-redux';
export function withIsAuthenticated(Component) {
function mapStateToProps(state) {
return {
isAuthenticated: !!state.auth.userInfo
};
}
return connect(mapStateToProps)(function({ isAuthenticated, ...props }) {
return <Component isAuthenticated={isAuthenticated} {...props}/>;
});
}
export function YourComponent({ isAuthenticated, ...props }) {
// Return your react sub-tree here based on `isAuthenticated`
// instead of `isAuthenticated()` like before.
}
Opinion
Personally, I feel that HOCs introduce a lot more complexity than necessary, and if I'm not mistaken, this was one of the primary drivers behind the creation of the Hooks API.

React Native - reduce render times to optimize performance while using React hooks

Background
After releasing React v16.8, now we have hooks to use in React Native.
I am doing some simple tests to see the render times and the performance between
Hooked functional components and class components. Here is my sample:
#Components/Button.js
import React, { memo } from 'react';
import { TouchableOpacity, Text } from 'react-native';
const Button = memo(({ title, onPress }) => {
console.log("Button render"); // check render times
return (
<TouchableOpacity onPress={onPress} disabled={disabled}>
<Text>{title}</Text>
</TouchableOpacity>
);
});
export default Button;
#Contexts/User.js
import React, { createContext, useState } from 'react';
import User from '#Models/User';
export const UserContext = createContext({});
export const UserContextProvider = ({ children }) => {
let [ user, setUser ] = useState(null);
const login = (loginUser) => {
if (loginUser instanceof User) { setUser(loginUser); }
};
const logout = () => {
setUser(null);
};
return (
<UserContext.Provider value={{value: user, login: login, logout: logout}}>
{children}
</UserContext.Provider>
);
};
export function withUserContext(Component) {
return function UserContextComponent(props) {
return (
<UserContext.Consumer>
{(contexts) => <Component {...props} {...contexts} />}
</UserContext.Consumer>
);
}
}
Cases
We have two cases below for constructing screen components:
#Screens/Login.js
Case 1: Functional Component with Hooks
import React, { memo, useContext, useState } from 'react';
import { View, Text } from 'react-native';
import Button from '#Components/Button';
import { UserContext } from '#Contexts/User';
const LoginScreen = memo(({ navigation }) => {
const appUser = useContext(UserContext);
const [foo, setFoo] = useState(false);
const userLogin = async () => {
let response = await fetch('blahblahblah');
if (response.is_success) {
appUser.login(user);
} else {
// fail on login, error handling
}
};
const toggleFoo = () => {
setFoo(!foo);
console.log("current foo", foo);
};
console.log("render Login Screen"); // check render times
return (
<View>
<Text>Login Screen</Text>
<Button onPress={userLogin} title="Login" />
<Button onPress={toggleFoo} title="Toggle Foo" />
</View>
);
});
export default LoginScreen;
Case 2: Component wrapped with HOC
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import Button from '#Components/Button';
import { withUserContext } from '#Contexts/User';
import UserService from '#Services/User';
class LoginScreen extends Component {
state = { foo: false };
userLogin = async () => {
let response = await UserService.login();
if (response.is_success) {
login(user); // function from UserContext
} else {
// fail on login, error handling
}
};
toggleFoo = () => {
const { foo } = this.state;
this.setState({ foo: !foo });
console.log("current foo", foo);
};
render() {
console.log("render Login Screen"); // check render times
return (
<View>
<Text>Login Screen</Text>
<Button onPress={userLogin} title="Login" />
<Button onPress={toggleDisable} title="Toggle" />
</View>
);
}
}
Results
Both cases have identical render times at the beginning:
render Login Screen
Button render
Button render
But while I press the "Toggle" button, the state changed and here is the result:
Case 1: Functional Component with Hooks
render Login Screen
Button render
Button render
Case 2: Component wrapped with HOC
render Login Screen
Questions
Although the Button Component isn't a large bunch of codes, considering the re-render times between two cases, Case 2 should have a better performance than Case 1.
However, considering the code readability, I definitely love using hooks more than using HOC. (Especially the function: appUser.login() and login())
So here's the question. Is there any solution that I can keep the benefits of both size, decreasing the re-render times while using the hooks? Thank you.
The reason that both button re-render even though you use memo in case of a functional component is because the function references are changed on each re-render as they are defined within the functional component.
Similar case will happen if you use arrow functions in render for a class component
In case of a class the function references don't change with the way you define them as functions are defined outside of your render method
To optimize on rerenders, you should make use of useCallback hook to memoize your function references
const LoginScreen = memo(({ navigation }) => {
const appUser = useContext(UserContext);
const [foo, setFoo] = useState(false);
const userLogin = useCallback(async () => {
let response = await fetch('blahblahblah');
if (response.is_success) {
appUser.login(user);
} else {
// fail on login, error handling
}
}, []); // Add dependency if need i.e when using value from closure
const toggleFoo = useCallback(() => {
setFoo(prevFoo => !prevFoo); // use functional state here
}, []);
console.log("render Login Screen"); // check render times
return (
<View>
<Text>Login Screen</Text>
<Button onPress={userLogin} title="Login" />
<Button onPress={toggleFoo} title="Toggle Foo" />
</View>
);
});
export default LoginScreen;
Also note that React.memo cannot prevent re-renders due to context value changes. Also note that while passing value to context provider you should make use of useMemo too
export const UserContextProvider = ({ children }) => {
let [ user, setUser ] = useState(null);
const login = useCallback((loginUser) => {
if (loginUser instanceof User) { setUser(loginUser); }
}, []);
const logout = useCallback(() => {
setUser(null);
}, []);
const value = useMemo(() => ({
value: user,
login: login,
logout: logout,
}), [user, login, logout]);
/*
Note that login and logout functions are implemented using `useCallback` and
are created on initial render only and hence adding them as dependency here
doesn't make a difference and will definitely not lead to new referecne for
value. Only `user` value change will create a new object reference
*/
return (
<UserContext.Provider value={value}>
{children}
</UserContext.Provider>
);
};
The reason is in functional component whenever the component re-render, new userLogin created => Button component is re-render.
const userLogin = async () => {
const response = await fetch("blahblahblah")
if (response.is_success) {
appUser.login(user)
} else {
// fail on login, error handling
}
}
You can use useCallback to memoize userLogin function + wrap Button component with React.memo (as what you did) prevent unwanted re-render:
const userLogin = useCallback(async () => {
const response = await fetch("blahblahblah")
if (response.is_success) {
appUser.login(user)
} else {
// fail on login, error handling
}
}, [])
The reason why it not happen in class component is when class component is re-rendered only render function is trigger (of course some other lifecycle function such as shoudlComponentUpdate, componentDidUpdate trigger too). ==> userLogin not change ==> Button component is not re-render.
This is great article to have a look about useCallback + memo
Notice: When you use Context, memo can not prevent the component, which is a Consumer, re-render if values of Context Provider changed.
For example:
If you call setUser in UserContext => UserContext re-render => value={{value: user, login: login, logout: logout}} change => LoginScreen re-render. You cannot use shouldComponentUpdate (class compoenent) or memo (functional component) to prevent re-render, because it's not update via props, it's updated via value of Context Provide

React hook "useSelector" is called in function error for my permissions context provider

I'm trying to make a permissions provider that wraps some react-redux global state. I have as follows:
import React, { useCallback } from 'react';
import { useSelector } from 'react-redux';
import { createSelector } from 'reselect';
export const PermissionsContext = React.createContext();
const getUserAbilities = createSelector(
state => state.user.abilities,
abilities =>
abilities.reduce((acc, val) => {
acc[val] = true;
return acc;
}, {})
);
function useAbilities() {
const abilities = useSelector(getUserAbilities);
return { abilities };
}
export const PermissionsProvider = ({ children }) => {
const { abilities } = useAbilities();
const can = useCallback((...permissions) => permissions.every(permission => permission in abilities), [
abilities
]);
return <PermissionsContext.Provider value={{ can }}>{children}</PermissionsContext.Provider>;
};
export const withPermissions = WrappedComponent => {
return class ComponentWithPermissions extends React.Component {
render() {
return (
<PermissionsContext.Consumer>
{props => <WrappedComponent {...this.props} permissions={props} />}
</PermissionsContext.Consumer>
);
}
};
};
Usage of PermissionsProvider:
<PermissionsProvider>
<App />
</PermissionsProvider>
This includes a context so I can useContext(PermissionsContext) and also a HOC withPermissions so that I wrap legacy class components with it.
In the case of a class, I would call this.props.permissions.can('doThing1', 'doThing2') and it should return true or false depending on whether all of those abilities are present in the user payload.
It seems to be functioning fine except when I try to commit it, I get the error:
React Hook "useSelector" is called in function "can" which is neither a React function component or a custom React Hook function react-hooks/rules-of-hooks
I saw a few issues with naming convention, but that doesn't seem to apply here(?). I also used to have the useAbilities hook inside the function just above the can function which also threw the error.
Any ideas?
All the above code was correct. It seemed to be some sort of eslint cache that was continuing to complain. After clearing cache and rerunning lint it went away.

Resources