Search code examples
reactjstypescriptreact-routerreact-router-domtsx

Nav Link does not trigger custom hook


I'm fairly new to React and am following a tutorial to train myself.

The problem I'm having is that I have several routes rendering the same component with different props, but the component doesn't seem to be mounted/dismounted when navigating using NavLink. So my custom hook isn't triggered and it's as if we haven't navigated.

If I manually refresh the page after navigation, the component is correctly updated. I use react@18.2.0 and react-router-dom@6.16.0.

  • index.tsx
    import React from 'react';
    import ReactDOM from 'react-dom/client';
    import { BrowserRouter } from "react-router-dom";
    import './index.css';
    import App from './App';
    
    const root = ReactDOM.createRoot(
      document.getElementById('root') as HTMLElement
    );
    root.render(
      <React.StrictMode>
        <BrowserRouter>
          <App />
        </BrowserRouter>
      </React.StrictMode>
    );
    
  • App.tsx
    import React from 'react';
    import { Header } from "./components";
    import { Route, Routes } from "react-router-dom";
    import { MovieList } from "./pages";
    
    function App() {
      return (
        <>
          <Header />
          <Routes>
            <Route path="/" element={<MovieList />} />
            <Route path="/movies/bottom" element={<MovieList sort='bottom' />} />
            <Route path="/movies/unpopular" element={<MovieList sort='unpopular' />} />
            <Route path="/movies/upcoming" element={<MovieList sort='upcoming' />} />
          </Routes>
        </>
      );
    }
    
    export default App;
    
  • Into the Header.tsx component, I have :
    <li>
      <NavLink to="/" className={linkClass} aria-current="page" end>Recent</NavLink>
    </li>
    <li>
      <NavLink to="/movies/unpopular" className={linkClass}>Unpopular</NavLink>
    </li>
    <li>
      <NavLink to="/movies/bottom" className={linkClass}>Bottom Rated</NavLink>
    </li>
    <li>
      <NavLink to="/movies/upcoming" className={linkClass}>Upcoming</NavLink>
    </li>
    
  • MovieList.tsx
    import { useState } from "react";
    import { Card } from "../components";
    import { MovieService } from "../services/Movie.service";
    
    type MovieListProps = {
      sort?: MovieService.defaultSort;
    }
    
    export function MovieList({ sort }: MovieListProps = {}) {
      const [filter, setFilter] = useState({
        defaultSort: sort
      });
      const movies = MovieService.useGet(filter);
    
      return (
        <main>
          <button onClick={() => setFilter({defaultSort: 'now_playing'})}>Recent</button>
          <button onClick={() => setFilter({defaultSort: 'unpopular'})}>unpopular</button>
          <button onClick={() => setFilter({defaultSort: 'bottom'})}>Bottom Ratted</button>
          <button onClick={() => setFilter({defaultSort: 'upcoming'})}>Upcoming</button>
          <section className="flex flex-wrap justify-center gap-4">
            {
              movies.data?.results.map((movie, index) => (
                <Card key={index} item={movie}/>
              ))
            }
          </section>
        </main>
      );
    }
    

Here, the movies object isn't updated when I click on the NavLink from the header, but I've added some buttons that call setFilter, and when I click on those buttons, it works fine, why?

You can find the code of the tutorial that works here : github

I have diff because I'm doing it with Typescript and I isolated the code to fetch the APIs in a different way, you can find my currently code here : github

But I still don't understand why my implementation doesn't work.


Solution

  • React-Router optimizes to keep React components mounted. In other words, it tries to not do any extra work to unmount and remount the same React component if it can help it. In React, triggering a component rerender is less effort than remounting.

    You can add a useEffect hook to update the filter state when the sort prop value changes:

    export function MovieList({ sort }: MovieListProps = {}) {
      const [filter, setFilter] = useState({ defaultSort: sort });
    
      useEffect(() => {
        setfilter({ defaultSort: sort });
      }, [sort]);
    
      const movies = MovieService.useGet(filter);
    
      return (
        <main>
          <button onClick={() => setFilter({ defaultSort: 'now_playing' })}>
            Recent
          </button>
          <button onClick={() => setFilter({ defaultSort: 'unpopular' })}>
            unpopular
          </button>
          <button onClick={() => setFilter({ defaultSort: 'bottom' })}>
            Bottom Ratted
          </button>
          <button onClick={() => setFilter({ defaultSort: 'upcoming' })}>
            Upcoming
          </button>
          <section className="flex flex-wrap justify-center gap-4">
            {movies.data?.results.map((movie, index) => (
              <Card key={index} item={movie} />
            ))}
          </section>
        </main>
      );
    }
    

    Or you could give each "instance" of the MovieList component a unique React key so the component remounts when the route changes.

    function App() {
      return (
        <>
          <Header />
          <Routes>
            <Route path="/" element={<MovieList key="none" />} />
            <Route
              path="/movies/bottom"
              element={<MovieList key="bottom" sort="bottom" />}
            />
            <Route
              path="/movies/unpopular"
              element={<MovieList key="unpopular" sort="unpopular" />}
            />
            <Route
              path="/movies/upcoming"
              element={<MovieList key="upcoming" sort="upcoming" />}
            />
          </Routes>
        </>
      );
    }