Dashiell Rose Bark-Huss
Dashiell Rose Bark-Huss

Reputation: 2965

How to set a NavLink to active conditionally on dynamic links

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?

Example

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?

CodeSandbox

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

Answers (2)

Dashiell Rose Bark-Huss
Dashiell Rose Bark-Huss

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;

codesandbox

Upvotes: 2

Drew Reese
Drew Reese

Reputation: 202667

Issue

Your auth state resides in your Profile component so it isn't reachable within the Nav component.

Solution

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.

  1. 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>
  1. 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>
      );
    }
    
  2. 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>
      );
    }
    

Demo

Edit how-to-set-a-navlink-to-active-on-conditionally-dynamic-links

enter image description here

Edit

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.

  1. 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>
    
  2. 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]);
    
  3. 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>
    

Edit how-to-set-a-navlink-to-active-on-conditionally-dynamic-links (forked)

Upvotes: 4

Related Questions