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