Async Route Element Return in React JS - javascript

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;

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;

ReactJS - Conditionally call hooks

With the risk of being a duplicate question, because I am sure that it has been asked several times, as seen from this popular post.
I have this case when I want to check first if it is authenticated and if not, it should skip the rest computations.
How can I utilize useEffect or some other hook for this problem, given the code below:
React Hook "useDispatch" is called conditionally. React Hooks must be called in the exact same order in every component render.
React Hook "useSelector" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
React Hook "useEffect" is called conditionally. React Hooks must be called in the exact same order in every component render. Did you accidentally call a React Hook after an early return?
Component
export const DoSomething: FunctionComponent = () => {
const isAuthenticated = useSelector<State, boolean>((state) => state.isAuthenticated);
if (isAuthenticated === false) {
return <Navigate to='/login' />
}
const dispatch = useDispatch();
const { initialDay: initialDay = '0' } = useParams();
useEffect(() => {
// Do another thing here
dispatch(foo());
}, [dispatch]);
return (
...
)
}
I tried to wrap with an if...else statement like this:
if (isAuthenticated){
const dispatch = useDispatch();
const { initialDay: initialDay = '0' } = useParams();
useEffect(() => {
// Do another thing here
dispatch(foo());
}, [dispatch]);
}
but the problem still persist
If you early return somethings before the hook is invoked, it breaks the rules of hook. If you want to check any conditions, put it inside the callback, it guarantee that the hook is called properly through every render.
export const DoSomething: FunctionComponent = () => {
const isAuthenticated = useSelector<State, boolean>((state) => state.isAuthenticated);
const dispatch = useDispatch();
const { initialDay: initialDay = '0' } = useParams();
useEffect(() => {
if (isAuthenticated === false) {
return;
} else {
dispatch(foo());
}
}, [dispatch]);
if (isAuthenticated === false) {
return <Navigate to='/login' />
}
return (
...
)
}

How do I solve the problem of Promise pending

I am currently trying to make a protected route function in react. I wish that result will return a boolean value rather than a promise without changing ProtectedRoute to async:
import React from "react";
import { Redirect, Route } from "react-router-dom";
import { CheckToken } from "./RequestAction"
function ProtectedRoute({ component: Component, ...restOfProps }) {
const result = (async () => {
const res = await CheckToken()
return res;
})()
console.log(result); //log-> PromiseĀ {<pending>}[[Prototype]]: Promise[[PromiseState]]: "fulfilled"[[PromiseResult]]: false
return (
<Route
{...restOfProps}
render={(props) =>
result ? <Component {...props} /> : <Redirect to="/login/" />
}
/>
);
}
export default ProtectedRoute;
This is the CheckToken function:
import axios from "axios";
async function CheckToken() {
let result = null;
await axios
.get(`http://localhost:5000/protected`,
{"withCredentials": true}
)
.then((res) => {
console.log("res.data.status:",res.data)
if (res.data.status === "success") {
result = true
}
})
.catch((err) => {
result = false
});
console.log("result:",result);
return result
}
const useToken = () => {
const [token, setToken] = useState(null);
useEffect(() => {
const f = async () => {
const res = await CheckToken();
//TODO: add check login;
setToken(res);
};
f();
})
return token;
}
function ProtectedRoute({ component: Component, ...restOfProps }) {
const token = useToken();
return (
<Route
{...restOfProps}
render={(props) =>
token ? <Component {...props} /> : <Redirect to="/login/" />
}
/>
);
}
Function component body cannot be async.
OK, so in your FunctionComponent you are trying to contact a server on a per render basis, and you are saying you want to capture the result in a boolean, that is then passed onto your child Route component
Firstly, network requests are considered effects, usually triggered by a change in your component props. you would want to handle the network behavior asynchronously as it would block the rendering of the component otherwise, which is something React doesn't allow. Also, you probably only want to check the token in specific circumstances, depending on your props.
You can still render your Route child component once your network request has resolved, but until then, there will be an intermediary state that your component will need to handle. How it does that is up to you, but you should probably not render a link until you have that result information.
I'd introduce a bit of state in a custom hook, and just have your component return out some intermediary state until you get a result / error.
I would also just return a token, you can use null to indicate the network request has not completed, and empty string if one hasn't been retrieved.
const useToken = () => {
const [state, setState] = useState(null)
useEffect(() => {
//TODO: you would also want to catch errors
CheckToken().then(setState)
})
return state
}
function ProtectedRoute({ component: Component, ...restOfProps }) {
const token = useToken()
if (token === null) {
return <Spinner/> // something that indicates you are waiting
}
return (
<Route
{...restOfProps}
render={(props) =>
token ? <Component {...props} /> : <Redirect to="/login/" />
}
/>)
}
}

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.

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

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,
};
};

Categories

Resources