I'm very new to React - so bear with me.
I'm trying to create a set of authentication protected routes/components. I have the below code that I am using to achieve that.
However, my issue is that when the child component loads, the userInfo is {} (i.e. not set). I know that the userInfo is being returned from the userService as my console.log returns the correct data.
Am I going about this right? I want to be able to protect a component/route, and pass through the userInfo to any protected route so I can do stuff with the data in the respective component.
const UserAuthenticatedRoute = ({component: Component, ...rest}) => {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [userInfo, setUserInfo] = useState({});
useEffect(async () => {
const r = await userService.isUserLoggedIn();
console.log(r.status);
if (r.status === 200){
setIsLoggedIn(true);
const userInfo = await r.json();
console.log(userInfo);
setUserInfo(userInfo);
} else {
setIsLoggedIn(false);
}
}, []);
return (
<Route {...rest} render={props => (
<>
<main>
{isLoggedIn &&
<Component {...props} userInfo={userInfo}/>
}
</main>
</>
)}
/>
);
};
Related
I am trying to implement Protected Routes in my app. I am using cookie-based session authentication.
The issue is: Whenever I try to access a protected page for the first time, the RequireAuth component has the isAuthenticated value as false and hence it navigates to /.
From the console logs, I can see Inside require auth. before Inside provide auth..
Questions:
Is using useEffect in the context provider the right way to set the auth state?
How do I make sure that the context provider state is set before accessing the context in the consumer RequireAuth?
I have a context provider ProvideAuth which makes an API call to check if the user is already authenticated.
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
Auth context consumer
export const useAuth = () => {
return useContext(authContext);
};
I use the context in a RequireAuth component to check if the user is already authenticated and redirect if not.
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo } = useAuth();
const location = useLocation();
useEffect(() => {
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, []);
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
The context provider is used in the App.js
return (
<ProvideAuth>
<div className='App'>
<Routes>
<Route exact path="/" element={<Home />} />
<Route path="/pricing" element={<Pricing />} />
<Route element={<RequireAuth /> }>
<Route path="/jobs" element={<Jobs />} >
<Route index element={<MyJobs />} />
<Route path="new" element={<NewJob />} />
<Route path=":jobId" element={<JobDetails />} />
<Route path=":jobId/stats" element={<JobStats />} />
</Route>
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
</div>
</ProvideAuth>
);
That's because that useEffect in ProvideAuth is as any useEffect an asynchronous task, which means the component and its children may render before its callback gets executed.
A solution is to set up a loading state in ProvideAuth, called for example isCheckingAuth, set to true by default, and to false after you have done all the fetching. And you pass it down to RequireAuth, like so :
const authContext = createContext();
export function ProvideAuth({ children }) {
const navigate = useNavigate();
const location = useLocation();
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [userInfo, setUserInfo] = useState({});
const fetchData = async () => {
const isAuthenticated = await CheckAuthentication();
setIsAuthenticated(isAuthenticated);
if (isAuthenticated) {
const userInfo = await GetUserInfo();
setUserInfo(userInfo);
}
setIsCheckingAuth(false)
}
useEffect(() => {
console.log("Inside provide auth. " + isAuthenticated + " " + location.pathname);
fetchData();
}, []);
const value = {
isAuthenticated,
userInfo,
isCheckingAuth
};
return <authContext.Provider value={value}>{children}</authContext.Provider>;
}
You use that isCheckingAuth inRequireAuth to show a loader while the fetching is being done, this way:
export default function RequireAuth({ children }) {
const { isAuthenticated, userInfo, isCheckingAuth } = useAuth();
const location = useLocation();
useEffect(() => {
if(isCheckingAuth) return;
console.log("Inside require auth. " + isAuthenticated + " " + location.pathname);
}, [isCheckingAuth]);
if(isCheckingAuth) return <div>Loading...</div>
return isAuthenticated === true ?
(children ? children : <Outlet />) :
<Navigate to="/" replace state={{ from: location }} />;
}
What you can do is check, If the request is processed or not. If processing show loader if any error shows some error msg or redirect. If everything is fine load provider.
const authContext = createContext();
export function ProvideAuth({ children }) {
const [state, setState] = useState({
user: null,
isAuthenticated: false,
isLoading: false,
error: null,
});
useEffect(() => {
const fetchData = async () => {
try {
const isAuthenticated = await CheckAuthentication();
if (isAuthenticated) {
const user = await GetUserInfo();
setState((prev) => ({ ...prev, isAuthenticated, user }));
}
} catch (error) {
setState((prev) => ({ ...prev, error }));
} finally {
setState((prev) => ({ ...prev, isLoading: false }));
}
};
fetchData();
}, []);
if (state.isLoading) return <Loading />;
if (state.error) return <ErrorMessage error={state.error} />;
return <authContext.Provider value={state}>{children}</authContext.Provider>;
}
I'm working on the web-app that has a role choice when you visit for the first time. After I choose the admin role for example, it sets the value of userRole in localStorage.
Here is App you can see that depending on the current role there are different routes.
After I have the role chosen, I'm redirected to the '/' route and I actually want to see the component that renders this route which is TableBoard in this case, but instead I firstly get render={() => <Container>its from app.tsx {route.component}</Container>}. So on the first render I only see its from app.tsx, then I refresh the page and everything is fine.
How to fix routing here?
Please ask if something is unclear.
function App() {
const currentRole = useReduxSelector(state => state.auth.currentRole);
const accessToken = localStorage.getItem('accessToken');
const role = localStorage.getItem('role');
const getCurrentRoutes = () => {
if (role === 'userCollaborator') {
return AccRoutes;
} else if (role === 'userAdmin') {
return AdminRoutes;
} else if (role === 'userHr') {
return HRRoutes;
} else if (role === 'userPartner') {
return Routes;
} else {
return RoutesAuth;
}
};
const routes = getCurrentRoutes();
let routesComponents;
if (currentRole && accessToken) {
routesComponents = routes?.map(route => {
return (
<Route
key={route.name}
exact
path={route.path}
render={() => <Container>its from app.tsx {route.component}</Container>}
/>
);
});
} else {
routesComponents = routes?.map(route => {
return (
<Route
key={route.name}
exact
path={route.path}
component={route.component}
/>
);
});
}
return (
<BrowserRouter>
<Provider store={store}>{routesComponents}</Provider>
</BrowserRouter>
);
}
export default App;
Component that should be rendered for the route I'm redirected:
export const TableBoard = () => {
return (
<Container>
<div>here I have a lot of other content so it should be rendered as props.children in Container</div>
</Container>
);
};
And the code for Components.tsx looks this way:
const Container: React.FC<ContainerProps> = ({children}) => {
return (
<div>
{children}
</div>
);
};
export default Container;
Because localStorage.getItem('role') won't make react rerender your component. You can make a provider to handle this situation.
For example:
// RoleProvider.jsx
const RoleContext = React.createContext();
export const useRoleContext = () => {
return React.useContext(RoleContext)
}
export default function RoleProvider({ children }) {
const [role, setRole] = React.useState();
React.useEffect(
() => {
setRole(localStorage.getItem('role'));
},
[setRole]
)
const value = React.useMemo(
() => ({
role,
setRole: (role) => {
localStorage.setItem('role', role);
setRole(role);
}
}),
[role, setRole]
);
return (
<RoleContext.Provider value={value}>
{children}
</RoleContext>
)
};
Then, you can put the RoleProvider above those children who need the role value. And use const { role } = useRoleContext() in those children. Since role is stored in a state. Whenever you update the role by the following code, those children who use const { role } = useRoleContext() will be rerendered.
const { setRole } = useRoleContext();
// Please put it in a useEffect or handler function
// Or this will cause "Too many rerenders"
setRole('Some Role You Want')
I need to render a component that has a route using react router. the first component has a button that when clicked needs to render another component that has state passed in from the first component. All objects and strings from the first component show in the console.log of the child component but it wont set state when I use setProfile(p).
const Member = (props)=> {
const [user, setUser] = useState({});
const [profile, setProfile] = useState({});
// run effect when user state updates
useEffect(() => {
const doEffects = async () => {
try {
const pro = socialNetworkContract.members[0]
console.log(pro)
const p = await incidentsInstance.usersProfile(pro, { from: accounts[0] });
const a = await snInstance.getUsersPosts(pro, { from: accounts[0] });
console.log(a)
console.log(p)
setProfile(p)
} catch (e) {
console.error(e)
}
}
doEffects();
}, [profile, state]);
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div class="container">
<a target="_blank">Name : {profile.name}</a>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
{p.message}
</tr>})}
</div>
)
}
export default Member;
This is the parent component I want to redirect from
const getProfile = async (member) => {
const addr = dispatch({ type: 'ADD_MEMBER', response: member })
console.log(member)
}
const socialNetworkContract = useSelector((state) => state.socialNetworkContract)
return (
<div>
{socialNetworkContract.posts.map((p, index) => {
return <tr key={index}>
<button onClick={() => getProfile(p.publisher)}>Profile</button>
</tr>})}
</div>
)
}
export default withRouter(Posts);
I have this component working when I don't have a dynamic route that needs data passing in from the parent component It's redirecting from.
This is my routes.js file
const Routes = () => {
return (
<Switch>
<Route path="/posts" exact component={Posts} />
<Route path="/member" exact component={Member} />
<Redirect exact to="/" />
</Switch>
)
}
export default Routes
https://codesandbox.io/s/loving-pine-tuxxb
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.
I have this user context wit useEffect for firebase authentication. The whole app is wrapped around this.
export const UserProvider = ({ children }) => {
const [currentUser, setCurrentUser] = useState()
useEffect(() => {
auth.onAuthStateChanged((user) => {
setCurrentUser(user)
})
}, [])
return (
<UserContext.Provider value={currentUser}>{children}</UserContext.Provider>
)
}
In my component, I check if the user is authenticated, and I use router history to redirect the user if not.
const history = useHistory()
const currentUser = useContext(UserContext)
const handleRedirect = () => {
return history.push('/login')
}
return (
<>
{currentUser ? (
<div>
<MiniDrawer></MiniDrawer>
<Container maxWidth="md">
<h1>
Hello {currentUser.displayName}
</h1>
<Container>
<h2>Team Activity</h2>
</Container>
</Container>
</div>
) : (
handleRedirect()
)}
</>
Whenever I reload the page and I am logged in it will first execute the handleredirect method and then it will go to the login Page for like half a second) specified in this method and it renders the correct component (first in the condition). What to do about it? It seems that it takes a while for the app to realize if it user exists or not.
useEffect(() => {
setTimeout(() => {
fire.auth().onAuthStateChanged((user) => {
this.setState( { stateChanged: true } )
});
}, 1000)
}, [])