can't perform a react state update on an unmounted component issue with useEffect - javascript

I'm trying to redirect my user to a private route. I'm using redux thunk to fetch user info from the database, with storeUser(), if the info exists then the user proceeds otherwise they get redirected back to the home page. However its not working as expected. Its redirecting back to the home page when It should be proceeding. I can do this using class based syntax and componentDidMount. I tried to counter this issue of no access to componentDidMount by using the authChecked state to determine when the component has finished rendering
const PrivateRoute = (props) => {
const [authChecked, handleAuthChecked] = useState(false);
const [isAuth, handleIsAuth] = useState(false);
useEffect(() => {
props
.storeUser()
.then(() => {
props.user.email ? handleIsAuth(true) : handleIsAuth(false);
handleAuthChecked(true);
})
.catch(() => {
handleAuthChecked(true);
});
}, [props]);
if (authChecked) {
return isAuth ? <props.component /> : <Redirect to="/" />;
}
return null;
};
const mapStateToProps = (state) => {
return {
user: state.user,
};
};
export default connect(mapStateToProps, { storeUser })(PrivateRoute);
The code will always redirect the user though. isAuth will never return true even though props.user.email is true. It runs and redirects before it has chance to run handleIsAuth(true)

You have 2 issues that may be causing the defects you see:
First issue is caused by function scope within useEffect and your callback for storeUser. Instead of relying on the callback to determine whether the user has an email address, just do that in your render condition and let redux + react render cycle help you out.
In addition, you should only call the storeUser action on mount. Not every time props updates.
For example:
const PrivateRoute = (props) => {
const [authChecked, handleAuthChecked] = useState(false);
useEffect(() => {
props
.storeUser()
.then(() => {
handleAuthChecked(true);
})
.catch(() => {
handleAuthChecked(true);
});
}, []);
if (authChecked) {
return !!props.user.email
? <props.component />
: <Redirect to="/" />;
}
return null;
};
const mapStateToProps = (state) => {
return {
user: state.user,
};
};

Related

React.js, Auth Component does not redirect properly

I have created this Auth Component and it works fine. Except that, It does not redirect if the unauthenticated user tries to visit /dashboard.
The backend upon receiving /api/me request, knows the user by having the cookie. So I have (Cookie-Session) Authentication technique.
export const UserContext = createContext();
const Auth = ({ children }) => {
const [user, setUser] = useState(null);
const [gotUser, setGotUser] = useState(false);
const navigate = useNavigate();
const getUser = async () => {
const res = await fetch('/api/me');
const data = await res.json();
setUser(data);
if (user) {
setGotUser(true);
}
};
useEffect(() => {
if (!gotUser) {
getUser();
}
}, [user, gotUser, navigate]);
if (!user) {
navigate('/login');
}
console.log(user);
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
So the main issue is that no redirection done. Also, The user passed to the context is not updated properly. Maybe because I am confused about what to use in useEffect .
Any help is appreciated.
Issues
There are a couple issues:
The "unauthenticated" state matches the "I don't know yet" state (i.e. the initial state value) and the component is redirecting too early. It should wait until the user state is confirmed.
The navigate function is called as an unintentional side-effect directly in the body of the component. Either move the navigate call into a useEffect hook or render the Navigate component to issue the imperative navigation action.
Solution
Use an undefined initial user state and explicitly check that prior to issuing navigation action or rendering the UserContext.Provider component.
const Auth = ({ children }) => {
const [user, setUser] = useState(); // <-- initially undefined
const navigate = useNavigate();
const getUser = async () => {
try {
const res = await fetch('/api/me');
const data = await res.json();
setUser(data); // <-- ensure defined, i.e. user object or null value
} catch (error) {
// handler error, set error state, etc...
setUser(null); // <-- set to null for no user
}
};
useEffect(() => {
if (user === undefined) {
getUser();
}
}, [user]);
if (user === undefined) {
return null; // <-- or loading indicator, spinner, etc
}
// No either redirect to log user in or render context provider and app
return user
? <Navigate to="/login" replace />
: <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
useEffect runs after your JSX is rendered, so as your code is made, on a page refresh this if (!user) that calls navigate('/login') will always pass, as before the useEffect does its work, user is null, that inital value you gave to useState. Yet it's not redirecting because navigate does not work inside JSX, it should be replaced with Navigate the component.
Also, in getUser, you have this if (user) juste after setUser(data), that wouldn't work well as user won't get updated immediately, as updating a state is an asynchronous task which takes effect after a re-redner .
To fix your problems you can add a checking state, return some loader while the user is being verified. Also you can optimise a little bit your code overall, like getting ride of that gotUser state:
export const UserContext = createContext();
const Auth = ({ children }) => {
const [user, setUser] = useState(null);
const [checking, setChecking] = useState(true);
const getUser = async () => {
try {
const res = await fetch("/api/me");
const data = await res.json();
setUser(data);
} catch (error) {
setUser(null);
} finally {
setChecking(false);
}
};
useEffect(() => {
if (!user) {
getUser();
}
}, [user]);
if (checking) {
return <p>Checking...</p>;
}
if (!user) {
return <Navigate to="/login" replace />
}
return <UserContext.Provider value={user}>{children}</UserContext.Provider>;
};
export default Auth;

Async Route Element Return in React JS

Getting Error: Guard(...): Nothing was returned from render. This usually means a return statement is missing.
I want to call the API in my guard component before returning the element to Route Element to check if the logged in user have access to specific route or not.
For that, I have implemented a guard. Here's the route file code.
export default function Router() {
return (
<Routes>
<Route path="organizations">
<Route path="list" element={<Guard><ListOrganization /></Guard>} />
</Routes >
);
}
Guard component code
const Guard = (props) => {
fetch('https://apiToCheckPermission.com')
.then(response => {
if (response.isPermission) {
return props.children;
}
})
};
export default Guard;
It's kind of a thing that, I want to implement async route element. But React is throwing error if I don't immediately return the element from Guard.
Can anyone please tell How to solve this error?
If Guard is supposed to be a React component then it needs to always return valid JSX. The Guard currently returns nothing. I.E. it would need to return something from the Promise chain and then return that from the function body.
To resolve use some local state to hold a confirmed/verified permission value and conditionally render the children prop or a fallback. A typical route protection implementation will wait to confirm a user's access then render either the children or redirect to the login page with the location being accessed so the user can be redirected back after authenticating.
Example:
const Guard = ({ children }) => {
const location = useLocation();
const [hasPermission, setHasPermission] = React.useState(); // <-- initially undefined
React.useEffect(() => {
);
fetch('https://apiToCheckPermission.com')
.then(response => {
setHasPermission(response.isPermission);
});
}, []);
if (hasPermission === undefined) {
return null; // or loading indicator, spinner, etc
}
return hasPermission
? children
: <Navigate to="/login" replace state={{ from: location }} />;
};
Try to define a state to handle the permission and useEffect to load the data:
const Guard = (props) => {
const [hasPermission, setHasPermission] = useState(false);
useEffect(() => {
const fetchPermission = async () => {
const response = await fetch('https://apiToCheckPermission.com');
setHasPermission(response.isPermission);
}
fetchPermission().catch(console.error);
}, []);
if (!hasPermission) return <>Unauthorized</>;
return props.children;
};
export default Guard;

Props defined by async function by Parent in UseEffect passed to a child component don't persist during its UseEffect's clean-up

Please consider the following code:
Parent:
const Messages = (props) => {
const [targetUserId, setTargetUserId] = useState(null);
const [currentChat, setCurrentChat] = useState(null);
useEffect(() => {
const { userId } = props;
const initiateChat = async (targetUser) => {
const chatroom = `${
userId < targetUser
? `${userId}_${targetUser}`
: `${targetUser}_${userId}`
}`;
const chatsRef = doc(database, 'chats', chatroom);
const docSnap = await getDoc(chatsRef);
if (docSnap.exists()) {
setCurrentChat(chatroom);
} else {
await setDoc(chatsRef, { empty: true });
}
};
if (props.location.targetUser) {
initiateChat(props.location.targetUser.userId);
setTargetUserId(props.location.targetUser.userId);
}
}, [props]);
return (
...
<Chat currentChat={currentChat} />
...
);
};
Child:
const Chat = (props) => {
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, []);
...
The issue I'm dealing with is that Child's UseEffect clean up function, which depends on the chatroom prop passed from its parent, throws a TypeError error because apparently chatroom is null. Namely, it becomes null when the parent component unmounts, the component works just fine while it's mounted and props are recognized properly.
I've tried different approaches to fix this. The only way I could make this work if when I moved child component's useEffect into the parent component and defined currentChat using useRef() which honestly isn't ideal.
Why is this happening? Shouldn't useEffect clean-up function depend on previous state? Is there a proper way to fix this?
currentChat is a dependency of that effect. If it's null, the the unsubscribe should just early return.
const {currentChat} = props;
useEffect(() => {
const unsubscribeFromChat = () => {
if(!currentChat) return;
try {
onSnapshot(
collection(database, 'chats', currentChat, 'messages'),
(snapshot) => {
// ... //
}
);
} catch (error) {
console.log(error);
}
};
return () => {
unsubscribeFromChat();
};
}, [currentChat]);
But that doesn't smell like the best solution. I think you should handle all the subscribing/unsubscribing in the same component. You shouldn't subscribe in the parent and then unsubscribe in the child.
EDIT:
Ah, there's a bunch of stuff going on here that's not good. You've got your userId coming in from props - props.location.targetUser.userId and then you're setting it as state. It's NOT state, it's only a prop. State is something a component owns, some data that a component has created, some data that emanates from that component, that component is it's source of truth (you get the idea). If your component didn't create it (like userId which is coming in on props via the location.targetUser object) then it's not state. Trying to keep the prop in sync with state and worry about all the edge cases is a fruitless exercise. It's just not state.
Also, it's a codesmell to have [props] as a dependency of an effect. You should split out the pieces of props that that effect actually needs to detect changes in and put them in the dependency array individually.

How to render a component just after an async method?

I have a custom route that renders a page or redirects the user to the login page based on if the user logged in or not.
const AuthenticatedRoute = ({ children, ...rest }) => {
const auth = useContext(AuthContext);
const [isAuthenticated, setIsAuthenticated] = useState(null);
useEffect(() => {
const getAuth = async () => {
const res = await auth.isAuthenticated();
setIsAuthenticated(() => res);
};
getAuth()
}, [auth]);
return (
<Route
{...rest}
render={() => {
return isAuthenticated ? (
<>{children}</>
) : (
<Redirect to="/login" />
);
}}
></Route>
);
};
As you see inside useEffect I run an async method. The problem is that whenever the component wants to mount, the default value of isAuthenticated will be used and redirects the user to the login page. I'm a little confused about how to handle this situation. I don't want the component to be rendered when the async method is not completely run.
i believe it will process all your code before sending html to client's browser.

What could be causing redux state load delay?

I have a simple placeholder component that just displays a loading screen. But in the background, it's making checks to be redirected to the correct path. Using react-redux I can select a property in my state.
const isAuthenticated = useSelector((state) => state.auth.isAuthenticated);
console.log(isAuthenticated)
useEffect(() => {
if (isAuthenticated) {
history.push("/home");
} else {
history.push("/login");
}
}, [isAuthenticated, history]);
return (
<Spin size="large" />
);
//Redux store config
const configureStore = (persistedState) => {
const store = createStore(
rootReducer,
persistedState,
applyMiddleware(thunkMiddleware)
);
store.dispatch(validateAuth());
return store;
};
But when I check the console the output below shows it might be loading the initial state then the updated state. What could be causing this delay? As it is always redirecting to /login
useEffect(() => {
const timer = setTimeout(() => {
if (isAuthenticated) {
console.log("I'm here")
} else {
history.push("/login");
}
}, 1000);
return () => clearTimeout(timer);
}, [isAuthenticated, history]);
I decided to wrap my useEffect action in a timeout to wait for the correct state to be loaded. If this is incorrect or not the best practice please let me know a better answer.

Categories

Resources