skip to Main Content

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 [email protected] and [email protected].

  • 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.

2

Answers


  1. 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>
        </>
      );
    }
    
    Login or Signup to reply.
  2. 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?

    The updates are not seen as the .map will never reflect them because the const movies you are trying to map are not in a state variable managed by the useState hook, so any changes in movies value after the initial render are being ignored. In order to solve this issue, you should create a useState hook, and then get the data (presumably a fetch so make sure to use async/await) and set the state value so that your .map function would rerender on a state change.

    const [movies, setMovies]
    ...
    setMovies(*desired value*)
    

    I’ve added some buttons that call setFilter, and when I click on those buttons, it works fine, why?

    When you update the filter state value with setFilter, it causes the component to rerender, and your updated const movies becomes visable as by that time it has a changed value.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search