Search code examples
javascriptreactjstypescriptreact-router-dom

Why is useParams returning undefined even with the correct syntax?


I know this question has been asked a lot, but I read every question and answer and my problem is not gone.

I'm trying to access http://localhost:3000/1, and the useParams should receive 1 to fetch data from an API, but I'm receiving undefined.

In Component.tsx, I'm using console.log to receive the useParams, but I'm getting undefined. All related files are posted as I'm using BrowserRouter correctly and other related React Router imports.

What is wrong with my code? Why am I not getting the correct params?

Component.tsx:

import { createContext, useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { CharacterSchema, PageSchema } from "../interfaces/characterInterfaces";
import { IGetAllCharacters } from "../interfaces/contextInterfaces";
import { IChildren } from "../interfaces/reactInterfaces";
import api from "../services/api";

export const GetAllCharactersContext = createContext<IGetAllCharacters>({} as IGetAllCharacters);

export const GetAllCharactersInfo = ({ children }: IChildren) => {
  const [charactersList, setCharactersList] = useState<CharacterSchema[]>([]);

  let navigate = useNavigate();

  const { page } = useParams();
  console.log(page);

  useEffect(() => {
    api
      .get(`/characters?page=${page}`)
      .then((res) => {
        setCharactersList(res.data.data);
        window.localStorage.setItem("lastPage", String(page));
        return res;
      })
      .catch((err) => console.error(err));
  }, [page]);

  const nextPage = () => {
    navigate(`/2`);
  };

  const prevPage = () => {
    navigate(`/1`);
  };

  return (
    <GetAllCharactersContext.Provider value={{ charactersList, nextPage, prevPage }}>
      {children}
    </GetAllCharactersContext.Provider>
  );
};

AllRoutes.tsx:

const AllRoutes = () => {
  return (
    <Routes>
      <Route path="/" element={<Navigate to="/1" />} />
      <Route path="/:page" element={<Home />} />
      <Route path="*" element={<Home />} />
    </Routes>
  );
};

export default AllRoutes;

index.tsx:

    import React from "react";
    import ReactDOM from "react-dom/client";
    import App from "./App";
    import { BrowserRouter } from "react-router-dom";
    import Providers from "./contexts/Providers";
    
    const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
    root.render(
      <React.StrictMode>
        <BrowserRouter>
          <Providers>
            <App />
          </Providers>
        </BrowserRouter>
      </React.StrictMode>
    );

Solution

  • Having this <Route path="/:page" element={<Home />} /> will let you consume page with useParams only in Home, the component rendered by Route for this specific url.

    A way to accomplish what you want is to move the fetching part inside Home. To do so, export as part of the context a function that fetches and updates the state:

    // ⚠️ Import what's needed
    export const GetAllCharactersContext = createContext<IGetAllCharacters>({} as IGetAllCharacters);
    export const GetAllCharactersInfo = ({ children }: IChildren) => {
      const [charactersList, setCharactersList] = useState<CharacterSchema[]>([]);
      const navigate = useNavigate();
      
      const fetchCharactersList = useCallback((page) => {
        api
          .get(`/characters?page=${page}`)
          .then((res) => {
            setCharactersList(res.data.data);
            window.localStorage.setItem("lastPage", String(page));
          })
          .catch((err) => console.error(err));
      }, []);
    
    
      const nextPage = () => {
        navigate(`/2`);
      };
    
      const prevPage = () => {
        navigate(`/1`);
      };
    
      return (
        <GetAllCharactersContext.Provider value={{ charactersList, fetchCharactersList, nextPage, prevPage }}>
          {children}
        </GetAllCharactersContext.Provider>
      );
    };
    

    And move that useEffect you had in the provider inside Home. Something like this:

    // ⚠️ Import what's needed
    export default function Home() {
      const { fetchCharactersList, charactersList } = useContext(GetAllCharactersInfo);
      const { page } = useParams();
    
      useEffect(() => {
        if (!page) return;
        fetchCharactersList(page);
      }, [page]);
    
      // render data
      return <div></div>;
    }