简体   繁体   中英

history.push() using react-router-dom works in some components but not others

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

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.

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

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

Then try again.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM