Reputation: 15
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>
);
}
Upvotes: 1
Views: 1003
Reputation: 652
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.
Upvotes: 1
Reputation: 9321
I don't see any problems with your code. Try this
npm uninstall react-router-dom && npm i react-router-dom
Then try again.
Upvotes: 0
Reputation: 1479
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}) => (
....
);
Upvotes: 0