1
votes

So as the title says. I'm using React-router-dom and so within my App.js file i have my Router set up containing a Switch and multiple Routes. From a couple of smaller components i have no problem using useHisory and history.push() to manipulate the history and navigate my app.

However within my App.js file it doesn't work and i get returned:

"TypeError: Cannot read property 'push' of undefined"

I'm at a loss as to what is the problem and any help would be much appriciated.

import React, { useState, useEffect } from "react";
import {
  BrowserRouter as Router,
  Route,
  Switch,
  useHistory,
} from "react-router-dom";
import styled from "styled-components";

import unsplash from "../api/unsplash";
import Header from "./Header";
import Customise from "./Customise";
import LandingPage from "./LandingPage";
import GameBoard from "./GameBoard";
import GameFinished from "./GameFinished";

function App() {
  const [searchImageTerm, setSearchImageTerm] = useState("south africa");
  const [images, setImages] = useState([]);
  const [randomisedImages, setRandomisedImages] = useState([]);

  const [roundStarted, setRoundStarted] = useState(false);
  const [firstSelectedTile, setFirstSelectedTile] = useState(null);
  const [secondSelectedTile, setSecondSelectedTile] = useState(null);
  const [matchedTiles, setMatchedTiles] = useState([]);
  const [endOfTurn, setEndOfTurn] = useState(false);

  const [score, setScore] = useState(0);
  const [minutes, setMinutes] = useState(2);
  const [seconds, setSeconds] = useState(0);
  const [difficulty, setDifficulty] = useState(8);

  const history = useHistory();

  useEffect(() => {
    getImages();
  }, [searchImageTerm, difficulty]);

  useEffect(() => {
    randomiseImagesWithID(images);
  }, [images]);

  useEffect(() => {
    if (minutes === 0 && seconds === 0) {
      finishGame();
    }
  }, [seconds, minutes]);

  const finishGame = () => {
    history.push(`/gamefinished`);
  };

  useEffect(() => {
    if (roundStarted) {
      let myInterval = setInterval(() => {
        if (seconds > 0) {
          setSeconds(seconds - 1);
        }
        if (seconds === 0) {
          if (minutes === 0) {
            clearInterval(myInterval);
          } else {
            setMinutes(minutes - 1);
            setSeconds(59);
          }
        }
      }, 1000);
      return () => {
        clearInterval(myInterval);
      };
    }
  });

  useEffect(() => {
    if (matchedTiles.length > 0 && matchedTiles.length === images.length / 2) {
      alert("YOU WON!");
    }
  }, [matchedTiles]);

  const getImages = async () => {
    const response = await unsplash.get("/search/photos", {
      params: { query: searchImageTerm, per_page: difficulty },
    });
    setImages(response.data.results);
  };

  const generateTileId = () => {
    return "tile_id_" + Math.random().toString().substr(2, 8);
  };

  const randomiseImagesWithID = (images) => {
    let duplicateImagesArray = [...images, ...images];
    var m = duplicateImagesArray.length,
      t,
      i;
    while (m) {
      i = Math.floor(Math.random() * m--);
      t = duplicateImagesArray[m];
      duplicateImagesArray[m] = duplicateImagesArray[i];
      duplicateImagesArray[i] = t;
    }

    let finalArray = [];
    for (let image of duplicateImagesArray) {
      finalArray.push({
        ...image,
        tileId: generateTileId(),
      });
    }
    setRandomisedImages([...finalArray]);
  };

  const startRound = () => {
    setRoundStarted(true);
  };

  const onTileClick = (tileId, id) => {
    // is the tile already paired && is the tile selected && is it the end of the turn?
    if (
      !matchedTiles.includes(id) &&
      tileId !== firstSelectedTile &&
      !endOfTurn
    ) {
      // find image id for first selcted id for comparrison
      const firstSelctedTileId = randomisedImages.find(
        (image) => image.tileId === firstSelectedTile
      )?.id;
      // if there is no selected tile set first selected tile
      if (!firstSelectedTile) {
        setFirstSelectedTile(tileId);
      } else {
        // if the second tile matches the first tile set matched tiles to include
        if (id === firstSelctedTileId) {
          setMatchedTiles([...matchedTiles, id]);
          // add points to score
          setScore(score + 6);
          // reset selected tiles
          setFirstSelectedTile(null);
        } else {
          // deduct points from score
          setScore(score - 2);
          // set and display second tile choice
          setSecondSelectedTile(tileId);
          // set end of turn so tiles cannot be continued to be selected
          setEndOfTurn(true);
          // reset all values after a few seconds
          setTimeout(() => {
            setFirstSelectedTile(null);
            setSecondSelectedTile(null);
            setEndOfTurn(false);
          }, 1500);
        }
      }
    }
  };

  const onResetClick = () => {
    randomiseImagesWithID(images);
    setFirstSelectedTile(null);
    setSecondSelectedTile(null);
    setMatchedTiles([]);
    setScore(0);
    setEndOfTurn(false);
  };

  return (
    <div>
      <Router>
        <Container>
          <Header
            onResetClick={onResetClick}
            score={score}
            seconds={seconds}
            minutes={minutes}
          />
          <Main>
            <Switch>
              <Route path="/gameboard">
                <GameBoard
                  images={randomisedImages}
                  onTileClick={onTileClick}
                  firstSelectedTile={firstSelectedTile}
                  secondSelectedTile={secondSelectedTile}
                  matchedTiles={matchedTiles}
                />
              </Route>
              <Route path="/customise">
                <Customise
                  setSearchImageTerm={setSearchImageTerm}
                  setDifficulty={setDifficulty}
                  setMinutes={setMinutes}
                  startRound={startRound}
                />
              </Route>
              <Route path="/gamefinished">
                <GameFinished />
              </Route>
              <Route path="/">
                <LandingPage startRound={startRound} />
              </Route>
            </Switch>
          </Main>
        </Container>
      </Router>
    </div>
  );
}

export default App;

const Container = styled.div`
  width: 100%;
  height: 100vh;
  display: grid;
  grid-template-rows: 7rem;
`;

const Main = styled.div`
  display: grid;
  grid-template-columns: auto;
`;

And to give an example of where my code is working as expected:

import React from "react";
import { useHistory } from "react-router-dom";
import styled from "styled-components";

function LandingPage({ startRound }) {
  const history = useHistory();

  const startGame = () => {
    history.push(`/gameboard`);
    startRound();
  };

  const customiseGame = () => {
    history.push("/customise");
  };

  return (
    <Container>
      <WelcomeText>
        <p>Match the tiles by picking two at a time.</p>
        <p>Gain points for a correct match but lose points if they dont.</p>
        <p>Good Luck!</p>
      </WelcomeText>
      <ButtonContainer>
        <GameButton onClick={() => startGame()}>Start</GameButton>
        <GameButton onClick={() => customiseGame()}>Customise</GameButton>
      </ButtonContainer>
    </Container>
  );
}
3
useHistory provides you history object from React Router context and this context will be set as soon as you write "<Router ....". App component doesn't have access to this context, so you can't use useHistory in App. - Ajeet Shah
Yes. Solution 1: Go to index.jsx file and write <BrowserRouter><App /></BrowserRouter>. Solution 2: Use Router, not BrowserRouter. And provide your own history object. Benefit: You can now import the history you created anywhere and do .push on it. - Ajeet Shah
Using BrowserRouter is more common. But there are no cons of using Router. I have been using Router because sometimes I need to access history in a file which is not a React component. Yes, First, I was about to ask why you have so many state vars in App component. So, yes, mostly we don't need "history" object in App (root) component. - Ajeet Shah
The correct answer is in the second comment: you can use useHistory hook only in the component which is wrapped by Router. Split your App component into several components and implement correct wrapping. - Pavlo Zhukov
Thats great to know. Haha yeah I'm still getting to grips with Redux. I'm nearly confident with vanilla redux but still need to seek out the documentation around redux with hooks. But that is something I want to come back and do with this app once I'm more familiar with Redux hooks. Thanks for all your help I think I have a much better understanding than If someone had simply provided an answer and as as someone self teaching thats so valuable! Have a great day. - svedrup

3 Answers

1
votes

The reason why you are getting TypeError: Cannot read property 'push' of undefined is because you have initialized/assigned history before render has returned (hence the router never populated.

const history = useHistory();

Change it to this and everything should be working as expected (Warning: I haven't tested it myself):

const finishGame = () => {
    const history = useHistory();
    history.push(`/gamefinished`);
  };

It will work because finishGame is only called inside useEffect which is called after the page is rendered.

0
votes

You can pass history prop one component to another component.

like

// First component

import { useHistory } from "react-router-dom";

const firstComponent = () => {
const history = useHistory();

return (
<SecondComponent history=history />
)
}

const SecondComponent = ({history}) => (
....
);
0
votes

I don't see any problems with your code. Try this

npm uninstall react-router-dom && npm i react-router-dom

Then try again.