【问题标题】:history.push() using react-router-dom works in some components but not othershistory.push() 使用 react-router-dom 在某些组件中有效,但在其他组件中无效
【发布时间】:2021-06-19 23:57:13
【问题描述】:

正如标题所说。我正在使用 React-router-dom,因此在我的 App.js 文件中,我设置了包含一个 Switch 和多个路由的路由器。从几个较小的组件中,我可以毫无问题地使用 useHisory 和 history.push() 来操纵历史并导航我的应用程序。

但是在我的 App.js 文件中它不起作用并且我被退回:

“TypeError: 无法读取未定义的属性‘push’”

我不知道是什么问题,任何帮助都会非常感激。

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;
`;

并举例说明我的代码在哪里按预期工作:

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

【问题讨论】:

  • useHistory 为您提供来自 React Router 上下文的历史对象,并且该上下文将在您编写“App 组件无权访问此上下文,因此您不能在 App 中使用 useHistory
  • 是的。解决方案 1:转到 index.jsx 文件并写入 &lt;BrowserRouter&gt;&lt;App /&gt;&lt;/BrowserRouter&gt;。解决方案 2:使用 Router,而不是 BrowserRouter。并提供您自己的history 对象。好处:您现在可以在任何地方导入您创建的history 并在其上执行.push
  • 使用BrowserRouter 更为常见。但是使用Router 没有任何缺点。我一直在使用Router,因为有时我需要在不是 React 组件的文件中访问history。是的,首先,我正要问为什么App 组件中有这么多状态变量。所以,是的,大多数情况下我们不需要App(根)组件中的“历史”对象。
  • 正确答案在第二条评论中:useHistory钩子只能在Router包裹的组件中使用。将您的 App 组件拆分为多个组件并实现正确的包装。
  • 很高兴知道。哈哈,是的,我仍然在掌握 Redux。我对 vanilla redux 几乎有信心,但仍然需要使用 hooks 查找有关 redux 的文档。但这是我想在我更熟悉 Redux 钩子后回来使用这个应用程序做的事情。感谢您提供的所有帮助,我认为我比如果有人简单地提供答案并且作为一个自学的人那么有价值!祝你有美好的一天。

标签: javascript reactjs react-router react-hooks


【解决方案1】:

您收到TypeError: Cannot read property 'push' of undefined 的原因是因为您在渲染返回之前已经初始化/分配了历史记录(因此路由器从未填充过。

const history = useHistory();

把它改成这个,一切都应该按预期工作(警告:我自己没有测试过):

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

它会起作用,因为finishGame 仅在页面渲染后调用的useEffect 内部调用。

【讨论】:

    【解决方案2】:

    您可以将 history 属性传递给一个组件到另一个组件。

    喜欢

    // First component
    
    import { useHistory } from "react-router-dom";
    
    const firstComponent = () => {
    const history = useHistory();
    
    return (
    <SecondComponent history=history />
    )
    }
    
    const SecondComponent = ({history}) => (
    ....
    );
    

    【讨论】:

      【解决方案3】:

      我认为您的代码没有任何问题。试试这个

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

      然后再试一次。

      【讨论】:

        猜你喜欢
        • 2019-02-09
        • 2021-07-19
        • 2021-02-01
        • 1970-01-01
        • 1970-01-01
        • 2022-11-22
        • 1970-01-01
        • 1970-01-01
        • 2017-04-05
        相关资源
        最近更新 更多