'Make react-router-dom v6 pass path as key to rendered element

I think I may need a paradigm shift in my thinking here, so I'm open to those kinds of answers as well here.

Consider the following simplified example:

export const App = (p: { apiClient: SomeApiClient }) => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/posts/:postId" element={<Post apiClient={p.apiClient} />} />
    </Routes>
  );
}

export const Home = () => {
  return <h1>Home!</h1>
}

export const Post = (p: { apiClient: SomeApiClient }) => {
  const { postId } = useParams();
  const [ state, setState ] = useState<PostState>({ status: "loading" });

  // When the component mounts, get the specified post from the API
  useEffect(() => {
    if (state.status === "loading") {
      (async () => {
        const post = await p.apiClient.getPost(postId);
        setState({ status: "ready", post });
      })();
    }
  })

  return (
    <h2>Posts</h2>
    {
      state.status === "loading"
      ? <p>Loading....</p>
      : <div className="post">
        <h3>{state.post.title}</h3>
        <div className="content">{state.post.content}</div>
      </div>
    }
  )
}

export type PostState =
  | { status: "loading" }
  | { status: "ready"; post: BlogPost };
export type BlogPost = { title: string; content: string };

This works fine the first time, but pretend there's a <Link /> on the page that goes to the next post. When I click that link, the URL changes, but the page content doesn't, because React Router is not actually re-mounting the <Post .../> component. That component correctly receives the updated postId and is re-rendered, but since it doesn't get re-mounted, the useEffect logic doesn't run again and the content stays the same.

I've been solving this very awkwardly by creating intermediary components like so:

export const App = (p: { apiClient: SomeApiClient }) => {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route path="/posts/:postId" element={<PostRenderer apiClient={p.apiClient} />} />
    </Routes>
  );
}

export const PostRenderer = (p: { apiClient: SomeApiClient }) => {
  const { postId } = useParams();
  return <Post key={postId} postId={postId} apiClient={p.apiClient} />
}

export const Post = (p: { postId: string; apiClient: SomeApiClient }) => {
  // ....
}

But I'm starting to get a lot of those, and literally all they do is take the param from the URL and use it as a key on the actual target component. I've read through the react-router-dom docs and am not finding anything that indicates there's a way to automate this. I must be thinking about this wrong.... Any suggestions are appreciated.



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source