Reputation: 11
I am building a web app using Meteor.js with React. The app has two types of users: "user" and "employer", each with their own routes (like different dashboards and other pages). So I have a UserRouter
, an EmployerRouter
, and an UnprotectedRouter
(for when the user is not logged in yet). I want to return a different router depending on which user is logged in (I have a "type" field to let me know which user type they are). The issue is that the router is not updating when a user logs in. Basically I need different routers depending not only on if the user is logged in or not, but what TYPE of user it is.
I tried to use useState
, but it wouldn't update the router. Then I thought because the browser reroutes after the user signs up, maybe it would rerun App.tsx
which would update the router to the correct one, but that did not work either. Right now when I start up the app, it renders the UnprotectedRouter
because I'm not logged in. Then when I go to the sign-up form and create a new user (which logs me in automatically), it still shows UnprotectedRouter
rather than UserRouter
.
This is what my code currently looks like:
export const UserRouter = () => {
return (
<Routes>
<Route path={RoutePaths.USER_HOME} element={<UserHome />} />
<Route path={RoutePaths.USER_PROFILE} element={<UserProfile />} />
<Route path={RoutePaths.EMPLOYER_HOME} element={<EmployerHome />} />
<Route path={RoutePaths.USER_SIGNUP} element={<UserSignUp />} />
<Route path={RoutePaths.EMPLOYER_SIGNUP} element={<EmployerSignUp />} />
<Route path={RoutePaths.USER_SIGNIN} element={<UserSignIn />} />
<Route path="/position/:postingId" element={<UserPosting />}/>
</Routes>
);
};
export const EmployerRouter = () => {
return (
<Routes>
<Route path={RoutePaths.USER_HOME} element={<EmployerDashboard />} />
</Routes>
);
};
export const UnprotectedRouter = () => {
return (
<Routes>
<Route path={RoutePaths.WELCOME_PAGE} element={<WelcomePage />} />
<Route path={RoutePaths.USER_SIGNUP} element={<UserSignUp />} />
<Route path={RoutePaths.EMPLOYER_SIGNUP} element={<EmployerSignUp />} />
</Routes>
);
};
const selectRouter = (userType: String) => {
switch (userType) {
case "user":
return <UserRouter />
case "employer":
return <EmployerRouter />
default:
return <UnprotectedRouter />
}
}
const RouterChoice = () => {
const isLoadingProfiles = useSubscribe("allUserProfiles");
const loggedInUsername = Meteor.user()?.username;
const userType = UserCollection.findOne({ username: loggedInUsername })?.type;
return selectRouter(userType)
}
export const App = () => {
const router = RouterChoice();
return (
<BrowserRouter>
<div>
{router}
</div>
</BrowserRouter>
)
};
This is what my sign-up method for users looks like:
const handleSignUp = (e: { preventDefault: () => void }) => {
e.preventDefault();
console.log("password is: " + password);
console.log("username is: " + username);
console.log("email is: " + email);
Accounts.createUser(
{
username: username,
password: password,
email: email,
},
(error) => {
if (error) {
console.log(error);
return;
} else {
console.log("success!");
}
}
);
Meteor.call("user-profile.createNewProfile", {
username: username,
name: name,
type: "user",
});
setName("");
setUsername("");
setEmail("");
setPassword("");
console.log("submitted!");
navigate(RoutePaths.USER_HOME);
};
Upvotes: 1
Views: 412
Reputation: 203417
The issue it seems is that you have incorrectly cased the RouterChoice
function name which "tricks" React, or rather "Rules of Hooks", into thinking that RouterChoice
is a React component instead of a regular callback function. RouterChoice
calls a useSubscribe
React hook, but because App
isn't triggered to rerender it doesn't call the useSubscribe
again because RouterChoice
is never "called" again as a React component.
Rename RouterChoice
to something like useRouterChoice
so it's called validly as a custom React hook.
const selectRouter = (userType: String) => {
switch (userType) {
case "user":
return <UserRouter />;
case "employer":
return <EmployerRouter />;
default:
return <UnprotectedRouter />;
}
};
const useRouterChoice = () => {
const isLoadingProfiles = useSubscribe("allUserProfiles");
const loggedInUsername = Meteor.user()?.username;
const userType = UserCollection.findOne({ username: loggedInUsername })?.type;
return selectRouter(userType);
};
export const App = () => {
const router = useRouterChoice();
return (
<BrowserRouter>
<div>
{router}
</div>
</BrowserRouter>
);
};
The issue you might run into now is when the auth state/condition changes that the UI and other routes might not be mounted and navigatable to just yet. For this it's better to unconditionally render all the routes and protect/guard access to the routes.
Here's a common route protection implementation using layout routes.
const ProtectedRoute = ({ type }: { type: "user" | "employer"}) => {
const isLoadingProfiles = useSubscribe("allUserProfiles");
const loggedInUsername = Meteor.user()?.username;
const userType = UserCollection.findOne({ username: loggedInUsername })?.type;
if (isLoadingProfiles) {
return null; // or loading indicator/spinner/etc
}
return userType === type
? <Outlet />
: <Navigate to="/login" replace />;
};
export const App = () => {
const router = useRouterChoice();
return (
<BrowserRouter>
<div>
<Routes>
{/* unprotected routes */}
<Route element={<ProtectedRoute type="user" />}>
{/* protected user routes */}
</Route>
<Route element={<ProtectedRoute type="employer" />}>
{/* protected employer routes */}
</Route>
</Routes>
</div>
</BrowserRouter>
);
};
If you want the "unprotected" routes to not be accessible by authenticated users then protect them with another layout route component that applies the inverse logic of the ProtectedRoute
component.
const AnonymousRoute = () => {
const isLoadingProfiles = useSubscribe("allUserProfiles");
const loggedInUsername = Meteor.user()?.username;
const userType = UserCollection.findOne({ username: loggedInUsername })?.type;
if (isLoadingProfiles) {
return null; // or loading indicator/spinner/etc
}
return !!userType
? <Navigate to={/* any safe authenticated/protected route path */} replace />
: <Outlet />;
};
export const App = () => {
const router = useRouterChoice();
return (
<BrowserRouter>
<div>
<Routes>
<Route element={<AnonymousRoute />}>
{/* unprotected routes */}
</Route>
<Route element={<ProtectedRoute type="user" />}>
{/* protected user routes */}
</Route>
<Route element={<ProtectedRoute type="employer" />}>
{/* protected employer routes */}
</Route>
</Routes>
</div>
</BrowserRouter>
);
};
Upvotes: 0
Reputation: 39
What I have rendered is on App.tsx you need to wrap
const router = RouterChoice();
By a
useEffect(() => {}, [])
Like this:
useEffect(() => {
const router = RouterChoice();
}, [Meteor.user()?.username])
For the dependency I used <<Meteor.user()?.username>> but you should use what changes when the user logs in with a different account.
Upvotes: 0