Warning: Cannot update a component while rendering a different component. ReactRedux.Consumer - reactjs

I am getting a warning in my web app and have read a lot of posts about the issue. Unfortunately I have not managed to resolve my problem and hope someone might have some advice. From what I can tell I need to find a way of dispatching to the store in a useEffect. But my efforts so far have been unsuccessful.
The warning says:
index.js:1 Warning: Cannot update a component (Connect(TabMenuComponent)) while rendering a different component (ReactRedux.Consumer). To locate the bad setState() call inside ReactRedux.Consumer, follow the stack trace as described in https://reactjs.org/link/setstate-in-render
The stack trace further points me to this file. It points to line 30 which is the store.dispatch line:
export const AuthRoute = ({ component: Component, roles, computedMatch, ignoreRedirection, ...rest }) => (
<Route exact {...rest} render={props => {
return <ReactReduxContext.Consumer>
{({ store }) => {
const user = store.getState().appData.user;
if (!user) {
auth.setRedirectUrl(window.location.pathname);
return <Redirect to={auth.loginUrl} />;
}
const redirectUrl = auth.getRedirectUrl();
if (redirectUrl && !ignoreRedirection) {
auth.removeRedirectUrl();
return <Redirect to={redirectUrl} />;
}
if (roles && roles.length && !roles.some(neededRole => user.roles.some(userRole => userRole === neededRole))) {
return <BaseLayout authError={stringKeys.error.unauthorized}></BaseLayout>;
}
store.dispatch({ type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
return <Component {...props} />;
}}
</ReactReduxContext.Consumer>;
}
} />
);

You are dispatching an action in the middle of a render which is not correct. What you should instead do is to create an HOC or a wrapper component to your input Component and dispatch the action once thee component is mounted
With class component wrapper:
class CompWithDispatch extends React.Component {
componentDidMount() {
const { store, type, url, path, params } = this.props;
store.dispatch({ type, url, path, params })
}
render() {
const { store, type, url, path, params , component: Component, ...rest} = this.props;
return <Component {...rest} />
}
}
With function component wrapper
const CompWithDispatch = (props) => {
const { store, type, url, path, params, component:Component, ...rest } = props;
useEffect(() => {
store.dispatch({ type, url, path, params })
}, []);
return <Component {...rest} />
}
and use it like
export const AuthRoute = ({ component, roles, computedMatch, ignoreRedirection, ...rest }) => (
<Route exact {...rest} render={props => {
return <ReactReduxContext.Consumer>
{({ store }) => {
const user = store.getState().appData.user;
if (!user) {
auth.setRedirectUrl(window.location.pathname);
return <Redirect to={auth.loginUrl} />;
}
const redirectUrl = auth.getRedirectUrl();
if (redirectUrl && !ignoreRedirection) {
auth.removeRedirectUrl();
return <Redirect to={redirectUrl} />;
}
if (roles && roles.length && !roles.some(neededRole => user.roles.some(userRole => userRole === neededRole))) {
return <BaseLayout authError={stringKeys.error.unauthorized}></BaseLayout>;
}
const additionalProps = { type: "ROUTE_CHANGED", url: computedMatch.url, path: computedMatch.path, params: computedMatch.params })
return <CompWithDispatch {...additionalProps} {...props} component={component}/>;
}}
</ReactReduxContext.Consumer>;
}
} />
);

Related

Setting a global user state once, with ID returned from useSession hook with NEXT NEXT-AUTH

I have build my authentication system with next-auth. The data object returned from the useSessionhook returns the ID, that I then need to pass into a fetchUser function, so that I may acquire the user details and set that to a global state so the user with details can be shared throughout all my child components.
My MainLayout component wraps all the children that need access to the user in state
export const UserContext = createContext<{ user: UserWithDetails | null }>({
user: null,
})
export const MainLayout: React.FC<Props> = ({ children }) => {
const { data } = useSession()
const [user, setUser] = useState<null | UserWithDetails>(null)
const [isLoading, setIsLoading] = useState<boolean>(false)
const router = useRouter()
const fetchUser = async (id: unknown) => {
setIsLoading(true)
const res = await fetch(PATHS.API.userById(id))
const { data, success, errorMessage }: ResForUserWithDetails =
await res.json()
if (!success) {
console.warn(errorMessage)
}
setUser(data)
setIsLoading(false)
}
useEffect(() => {
if (data && !user) fetchUser(data.id)
}, [data, user])
return (
<UserContext.Provider value={{ user }}>
<Box h="100vh" bg="primary.100">
<Header />
{isLoading ? <Loader /> : children}
<Nav currentRoute={router.pathname} options={NAV_MENU_OPTIONS} />
</Box>
</UserContext.Provider>
)
}
The problem with this execution is the createContext is always initially set to null when the component mounts. As I toggle between children components the user is always refetched. My end goal is to set it once and leave it alone unless a refresh is triggered. If a user is accessing MainLayout they have already been authenticated and a valid JWT is present.
I know this question may be loaded but there is there any convention which supports setting the user to state only once instead of on every render.
This is my AUTH system and its application.
export const RequireAuth = ({
children,
}: {
children: ReactElement<any, any>
}) => {
const router = useRouter()
const { status } = useSession({
required: true,
onUnauthenticated() {
router.push(PATHS.LOGIN)
},
})
if (status === 'loading') {
return <Loader />
}
return children
}
__app.tsx
const CustomComponent: NextPage = () => {
if (OMIT_LAYOUTS[router.pathname]) {
return <Component {...pageProps} />
} else {
return (
<MainLayout>
<Component {...pageProps} />
</MainLayout>
)
}
}
return (
<SessionProvider session={session} refetchOnWindowFocus={true}>
<ChakraProvider theme={theme}>
{Component.auth ? (
<RequireAuth>
<CustomComponent />
</RequireAuth>
) : (
<CustomComponent />
)}
</ChakraProvider>
</SessionProvider>
)
}

how to delay the rendering of render function until usereffect calls in react

i want to run the useEffect first before the render function which is placed inside the <Route /> tag starts to render. i expect to get currently available user details through the API and assigne them to render function.
but render function runs before the UseEffect retrieve data from the API. so help me to find the solution.
import React, { useEffect, useState } from "react";
import { Route, Redirect } from "react-router-dom";
import { Auth } from "aws-amplify";
const ProtectedRoute = ({ children, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(
Auth.currentAuthenticatedUser({
// bypassCache: false,
})
.then((user) => console.log(user))
.catch((err) => console.log(err))
);
}, []);
return (
<Route
{...rest}
render={({ location }) =>
(isAuthenticated ) ? (
children
) : (
<Redirect
to={{
// pathname: "/login",
pathname: "/create-profile",
state: { from: location },
}}
/>
)
}
/>
);
};
export default ProtectedRoute;
Try this
useEffect(() => {
Auth.currentAuthenticatedUser({
// bypassCache: false,
})
.then((user) => user && setIsAuthenticated(true))
.catch((err) => err && setIsAuthenticated(false));
}, []);
You could wrap that authentication stuff into a hook of your own, and then simply not render anything until it's ready:
function useIsAuthenticated() {
const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => {
Auth.currentAuthenticatedUser({})
.then(setIsAuthenticated)
.catch((err) => {
console.log(err);
setIsAuthenticated(false);
});
}, []);
return isAuthenticated;
}
const ProtectedRoute = ({ children, ...rest }) => {
const isAuthenticated = useIsAuthenticated(); // Will be the user if authenticated, null if busy, or false if error.
if (isAuthenticated === null) {
return null; // Don't render anything if authentication state is unknown
}
return <>...</>;
};

How can I dynamically generate a PrivateRoute redirect based on an axios response?

I have an endpoint in my app called /get-redirect, which basically redirects you to wherever you need to be.
E.g. if you're not logged in, the response from this endpoint is an HTTP 200 with the redirect_location field in the response set to mysite.com/login. If you're logged in, but haven't completed step 2 of onboarding, it sends an HTTP 200 with redirect_location set to mysite.com/step2, etc.
I want to use PrivateRoute in React Router for authenticated pages, and I want any redirects to go to the result of the /get-redirect endpoint. I would then render the appropriate component in the statement.
This is what I have so far but I keep getting that the getPage() function is returning undefined. What am I missing here?
const fakeAuth = {
getPage(cb) {
if (document.cookie === null) {
return '/login'
}
const url = "https://api.mysite.com/get-redirect"
axios.get(url)
.then(function (response) {
return response.redirect_location
}).catch(function (error) {
if (error.response.status === 401) {
return '/401'
}
return '/404'
})
},
}
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => {
return <Redirect to={{
pathname: fakeAuth.getPage()
}} />
}} />
)
}
class App extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<Router>
<Switch>
...
<PrivateRoute path="/dashboard">
<Dashboard />
</PrivateRoute>
....
Issue
getPage needs to return the Promise chain started by axios.
You will need to wait for the Promise to resolve (or reject) in the PrivateRoute component but you can't just return the result as the redirect target.
Solution
I suggest abstracting a component to do the endpoint check and conditionally render the redirect or the original children prop. While the endpoint is checked conditionally render some loading state or null until the expected result is returned then conditionally render the redirect or child Dashboard component.
Example
const CheckRedirect = ({ children }) => {
const [isChecking, setIsChecking] = React.useState(true);
const [target, setTarget] = React.useState("/");
React.useEffect(() => {
fakeAuth
.getPage()
.then((target) => {
setTarget(target);
})
.finally(() => setIsChecking(false));
}, []);
if (isChecking) return "...Checking"; // or loading spinner, or null
return target.redirect_location ? (
<Redirect to={target.redirect_location} />
) : (
children
);
};
function PrivateRoute({ children, ...rest }) {
return (
<Route {...rest} render={() => <CheckRedirect children={children} />} />
);
}
Demo
For demo this is the mocked auth getPage component, it has a 50% chance to return redirect target.
const fakeAuth = {
getPage(cb) {
return new Promise((resolve) => {
setTimeout(() => {
return resolve(
Math.random() < 0.5 ? { redirect_location: "/step2" } : {}
);
}, 3000);
});
}
};

How do you wrap a React state variable in a function call?

I have some state that gets updated async that I would like to wrap inside a function call (to modularize the code).
Concretely, I have a isLoggedIn state variable that I would like to retrieve with a function call inside a React component. So far I have:
firebase.js
function userLoggedIn() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
firebase.auth().onAuthStateChanged((user) => {
if (user == null) {
console.log("No user logged in.");
} else {
console.log(`User ${user.uid} authenticated!`);
setIsLoggedIn(true);
}
});
return isLoggedIn;
}
export default userLoggedIn;
And I have a separate component that I would like to get this isLoggedIn state from:
AuthenticatedRoute.js
const AuthenticatedRoute = ({ component: Component, ...rest }) => {
const isLoggedin = userLoggedIn();
return (
<Route
{...rest}
render={(props) =>
isLoggedIn ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
/>
);
};
It works fine on page load, but if I log out (and user = null) isLoggedIn doesn't change to False.
This all works fine if I inline the function body inside AuthenticatedRoute: in general, how does one achieve this?

React private route function is called twice

If user is logged in, render the component. If not, render login page. I notice, however, that this function is called twice. The first time, useAuthDataContext() is null. The second time, I get the correct object back.
const PrivateRoute = ({ component, ...options }) => {
const { userData } = useAuthDataContext()
console.log(userData)
const finalComponent = userData != null ? component : Login
return (
<Route {...options} component={finalComponent} />
)
};
export default PrivateRoute
I have rewritten this function as follows. Here, PrivateRoute2 is called only once, and useAuthDataContext() returns null.
const PrivateRoute2 = ({ component: Component, ...rest }) => {
const { userData } = useAuthDataContext()
console.log(userData)
return (
<Route
{...rest}
render={props =>
userData != null ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
/>
)
}
Here is my useAuthDataContext() implementation that is causing the rerender:
export const AuthDataContext = createContext(null)
const initialAuthData = {}
const AuthDataProvider = props => {
const [authData, setAuthData] = useState(initialAuthData)
useLayoutEffect( (props) => {
const getUser = async () => {
try {
const userData = await authService.isAuthenticated()
setAuthData( {userData})
} catch (err) {
setAuthData({})
}
}
getUser()
}, [])
const onLogout = () => {
setAuthData(initialAuthData)
}
const onLogin = newAuthData => {
const userData = newAuthData
setAuthData( {userData} )
}
const authDataValue = useMemo(() => ({ ...authData, onLogin, onLogout }), [authData])
return <AuthDataContext.Provider value={authDataValue} {...props} />
}
export const useAuthDataContext = () => useContext(AuthDataContext)
export default AuthDataProvider
I think i found one solution. See this post https://hackernoon.com/whats-the-right-way-to-fetch-data-in-react-hooks-a-deep-dive-2jc13230

Resources