Reputation: 2965
In my menu, I have a navigation link- My Profile. When the user clicks My Profile react redirects to their profile. Then the nav link is set to active as expected, likewise, the isActive
style is applied.
<NavLink className="nav-link" to="/my_profile" activeStyle={isActive}>
My Profile
</NavLink>
A user can navigate to users' profiles by navigating to www.sitename.com/***username*** (like Twitter or Instagram).
<Route path="/:profile_name">
<Profile />
</Route>
When a user is logged in and navigates to their own profile through this dynamic route, I want the nav link for My Profile to be set to active. But I can only figure out how to set My Profile to active when My Profile is clicked on, not when the user navigates to their own profile through the address bar.
How can I set the NavLink
for My Profile to active when a user navigates to their dynamic profile route through the address bar?
In my code example, I have the currentUser
hard coded in as dash123
, so when we navigate to http://somedomain.com/dash123
the Profile
component recognizes /dash123
is the current user's profile and sets the state authorized
to true. When authorized is true
, Profile
renders to display "Edit your profile". Can I also make it so when authorized
is true
, the NavLink
for My Profile is set to active?
Code:
import React, { useState, useEffect } from "react";
import {
NavLink,
BrowserRouter as Router,
Route,
Switch,
useParams
} from "react-router-dom";
function Nav() {
const isActive = {
fontWeight: "bold",
backgroundColor: "lightgrey"
};
return (
<ul className="navbar-nav mr-auto">
<li className="nav-item">
<NavLink className="nav-link" to="/Shop" activeStyle={isActive}>
Shop
</NavLink>
</li>
<li className="nav-item">
<NavLink className="nav-link" to="/my_profile" activeStyle={isActive}>
My Profile
</NavLink>
</li>
</ul>
);
}
function Shop(props) {
return (
<div>
Lots of items to buy
<br /> Shoes $4.99
<br /> Carrots $9.99
<br /> Teslas $800,000
</div>
);
}
function Profile(props) {
let { profile_name } = useParams();
const [profile, setProfile] = useState();
const [authorized, setAuthorized] = useState(null);
// fake calls to database
const currentUser = () => {
return { name: "Dashie" };
};
const userInfo = (username) => {
const db = [
{ username: "dash123", name: "Dashie" },
{ username: "bob123", name: "Bob" }
];
return db.find((userDoc) => userDoc.username === username);
};
useEffect(() => {
if (props.profile === "currentUser") {
setProfile(currentUser());
} else {
setProfile(userInfo(profile_name));
}
}, [profile_name, props.profile]);
useEffect(() => {
if (profile && profile.name === currentUser().name) {
setAuthorized(true);
}
}, [profile]);
return (
<div>
Profile:
<br />
{profile && <>Name: {profile.name}</>}
<br />
{authorized && profile && <>Edit your profile, {profile.name}</>}
</div>
);
}
export default function App() {
return (
<div className="App">
<Router>
<Nav />
<Switch>
<Route path="/shop">
<Shop />
</Route>
<Route path="/my_profile">
<Profile profile={"currentUser"} />
</Route>
<Route path="/:profile_name">
<Profile />
</Route>
</Switch>
</Router>
<h2>Problem</h2>
<p>
If you click on <b>Shop</b>, as expected the <b>Shop</b> nav link will
be highlighted.
<br />
<br /> Likewise, the <b>My Profile</b> nav link will be highlighted when
we click on it and navigate to the user's profile.
<br />
<br /> But, we also want to have <b>My Profile</b> highlighted when we
navigate to,{" "}
<i>
"https://whateverdomainyouron/<b>dash123</b>"
</i>{" "}
since this takes us to the current user's profile.
</p>
</div>
);
}
Upvotes: 3
Views: 6207
Reputation: 2965
I took Drew Reese's suggestions and expanded on them to fit my needs.
I made it so the My Profile link is active if isProfileRoute
and currentUsersProfile
is met.
<NavLink
isActive={() => isProfileRoute && currentUsersProfile}
className="nav-link"
to="/my_profile"
activeStyle={isActive}
>
currentUsersProfile
is on the AuthContext.
const AuthContext = createContext({
authorized: false,
profile: null,
currentUsersProfile: null,
setAuthorized: () => {},
setProfile: () => {},
setCurrentUsersProfile: () => {},
});
The currentUsersProfile
is set in Profile
to true when the current profile belongs to the current logged in user.:
useEffect(() => {
if (profile?.name === currentUser().name) {
setAuthorized(true);
setCurrentUsersProfile(true);
} else {
setCurrentUsersProfile(false);
}
}, [profile]);
I create an array of all the routes:
const Routes = (
<Switch>
<Route path="/shop">
<Shop />
</Route>
<Route path={"/pro"}>
<Profile />
</Route>
<Route path={["/:profile_name", "/my_profile"]}>
<Profile />
</Route>
</Switch>
);
const array = Routes.props.children.map((child) => child.props.path);
// outputs: ['/shop', '/pro', ["/:profile_name", "/my_profile"]]
In Nav
I use this array to check to see if the current route the user is on is a the profile route (ex: '/dash123', '/somename', or the fixed route '/my_profile') using useMatchRoute
function Nav() {
const { currentUsersProfile } = useContext(AuthContext);
const allNonProfileRoutes = array.slice(0, -1);
let nonProfileRoute = useRouteMatch([...allNonProfileRoutes, { path: "/" }]);
const isProfileRoute = !nonProfileRoute.isExact;
Upvotes: 2
Reputation: 202667
Your auth state resides in your Profile
component so it isn't reachable within the Nav
component.
Refactor your code to lift the authentication state above the Nav
component and pass it as a prop (or consume in context) and set the active link state accordingly. The following solution uses a React Context.
Create an AuthContext
and provider component.
const AuthContext = createContext({
authorized: false,
profile: null,
setAuthorized: () => {},
setProfile: () => {}
});
const AuthProvider = ({ children }) => {
const [profile, setProfile] = useState();
const [authorized, setAuthorized] = useState(null);
const value = {
authorized,
profile,
setAuthorized,
setProfile
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
Wrap the Router
with the AuthProvider
<AuthProvider>
<Router>
<Nav />
<Switch>
<Route path="/shop">
<Shop />
</Route>
<Route path="/my_profile">
<Profile profile={"currentUser"} />
</Route>
<Route path="/:profile_name">
<Profile />
</Route>
</Switch>
</Router>
</AuthProvider>
Consume the AuthContext
in Profile
to set auth state. I.E. swap the old useState
hooks for the useContext
hook.
function Profile(props) {
const { profile_name } = useParams();
const { authorized, profile, setAuthorized, setProfile } = useContext(
AuthContext
);
...
return (
<div>
Profile:
<br />
{profile && <>Name: {profile.name}</>}
<br />
{authorized && profile && <>Edit your profile, {profile.name}</>}
</div>
);
}
Consume the AuthContext
in Nav
to ready the auth state and set the NavLink
active state accordingly.
function Nav() {
const { authorized } = useContext(
AuthContext
);
const isActive = {
fontWeight: "bold",
backgroundColor: "lightgrey"
};
return (
<ul className="navbar-nav mr-auto">
<li className="nav-item">
<NavLink className="nav-link" to="/Shop" activeStyle={isActive}>
Shop
</NavLink>
</li>
<li className="nav-item">
<NavLink
isActive={() => authorized} // <-- pass isActive callback
className="nav-link"
to="/my_profile"
activeStyle={isActive}
>
My Profile
</NavLink>
</li>
</ul>
);
}
Only I still see a complication. Once we navigate to /dash123 the authorized context is set, and My Profile active- as I want-good. But if we then click "Shop", Shop becomes active but My Profile is still active and it shouldn't be. We could fix this by changing the authorized context in the Shop component. It seems this could get out of hand because we'd have to access and change the context from every component that is linked or redirect coming from Profile. Is there a simpler solution or is this just a complication that's to be expected?
Best I could do for this is to simplify the routes a bit by nesting user profile ids under the "/my_profile" path, and applying a bit more logic on the path matching in the Nav
component.
Consolidate the profile routes into a single Route
that renders for both URLs. Notice here that order of path matching still matters, specifying the more specific path first.
<Route path={["/my_profile/:profile_name", "/my_profile"]}>
<Profile />
</Route>
Instead of passing a profile
prop to Profile
, provide it as a default param value. Update the useEffect
logic to use profile_name
.
const { profile_name = "currentUser" } = useParams();
...
useEffect(() => {
if (profile_name === "currentUser") {
setProfile(currentUser());
} else {
setProfile(userInfo(profile_name));
}
}, [profile_name, props.profile]);
Use the useRouteMatch
hook to match the "/my_profile" path prefix and adjust the isActive
callback logic. The link should be active if an exact match to "/my_profile", or an in-exact match and user authenticated.
const match = useRouteMatch("/my_profile");
...
<NavLink
isActive={() => match?.isExact || (match && authorized)}
className="nav-link"
to="/my_profile"
activeStyle={isActive}
>
My Profile
</NavLink>
Upvotes: 4