使用React Router与useLocation的错误处理及路由传参疑问
井字棋游戏路由状态缺失问题解决
问题场景
开发井字棋游戏时,介绍页(路径/)提供了选择pickIndex的组件,点击<ButtonMultiplayer />或<ButtonCPU />按钮后,会携带该值跳转到/game页面。正常流程下代码可正常运行,但出现两种异常情况:
- 未选择
pickIndex就手动修改URL直接访问/game,会抛出Cannot read properties of null (reading 'pickIndex')错误 - 在
/game页面时,先跳转到无效URL触发404,再切回/game,pickIndex会重置为null
相关代码
介绍页组件(IntroView):
export default function IntroView() { const id = useId(); const [isDisabled, setIsDisabled] = useState(true); const [pickIndex, setPickIndex] = useState(""); const levelRef = useRef("") const navigate = useNavigate(); return ( <div className={styles.mainContainer}> <form> <div className={styles.pickMarkBox}> <p>PICK PLAYER 1'S MARK</p> <div className={styles.buttonBox}> <ButtonMark classInput="P1" value="xButton" classLabel={styles.player1} markId={id + 'x'} isActive={pickIndex === 'x'} onClick={() => { setPickIndex('x'); setIsDisabled(false); }} /> <ButtonMark classInput="P2" value="zeroButton" classLabel={styles.player2} markId={id + 'zero'} isActive={pickIndex === 'zero'} onClick={() => { setPickIndex('zero'); setIsDisabled(false); }} /> </div> <p>REMEMBER: X GOES FIRST</p> </div> < ButtonMultiplayer value="NEW GAME VS PLAYER" className={`${styles.gameMode} ${styles.newGameVsPlayer}`} disabled={isDisabled} onClick={(e) => { e.preventDefault() navigate("/game", { state: { gameMode: 'vsPlayer', pickIndex } }); }} /> <ButtonCPU disabled={isDisabled} onChange={(e) => { e.preventDefault() levelRef.current = e.target.value navigate("/game", { state: { gameMode: 'vsCPU', pickIndex, levelRef } }) }} value={levelRef} /> </form > </div > ); }
Game页面组件(Board):
export default function Board(src) { const mark = useLocation().state.pickIndex; // ... 其他代码 }
用户问题
- 如何处理用户直接访问
/game的错误情况? - 是否有必要放弃
useLocation转而使用useParams?
无效尝试
- 直接在组件内写条件判断跳转(违反React Hooks调用规则):
if (useLocation().state.pickIndex === null) { navigate("/") }
- 使用
sessionStorage存储状态,但未生效
解决方案
1. 修复/game页面的状态校验与跳转
要遵循React Hooks的调用规则,通过useEffect处理路由状态校验,同时使用可选链避免空值报错:
import { useLocation, useNavigate, useEffect } from 'react-router-dom'; export default function Board() { const location = useLocation(); const navigate = useNavigate(); useEffect(() => { // 校验state是否存在,且pickIndex为有效值 const validPickValues = ['x', 'zero']; if (!location.state?.pickIndex || !validPickValues.includes(location.state.pickIndex)) { navigate('/'); } }, [location, navigate]); // 安全获取pickIndex,添加默认值兜底 const mark = location.state?.pickIndex || 'x'; // ... 其他游戏逻辑 }
说明:
useEffect会在路由变化时触发,确保每次进入/game都检查状态合法性- 可选链
?.避免location.state为null时直接访问pickIndex报错 - 增加有效值校验,防止传入非法的
pickIndex
2. useLocation vs useParams的选择
两种方案适用于不同场景:
继续使用useLocation
- 优势:状态存在内存中,不会暴露在URL里,适合不需要分享游戏链接的场景
- 优化方案:结合
sessionStorage持久化状态,避免刷新或从404切回时丢失:
在介绍页选择pickIndex时存入缓存:
在Board组件中读取缓存兜底:// IntroView中ButtonMark的点击事件 onClick={() => { setPickIndex('x'); setIsDisabled(false); sessionStorage.setItem('tictactoe_pickIndex', 'x'); }}useEffect(() => { const savedPick = sessionStorage.getItem('tictactoe_pickIndex'); if (!location.state?.pickIndex && !savedPick) { navigate('/'); } }, [location, navigate]); const mark = location.state?.pickIndex || sessionStorage.getItem('tictactoe_pickIndex') || 'x';
改用useParams
- 优势:参数存在URL中,用户可以直接分享带参数的游戏链接,直接访问也能获取到状态
- 实现步骤:
- 修改路由配置,添加参数占位符:
// 路由配置示例 <Route path="/game/:pickIndex/:gameMode" element={<Board />} /> - 介绍页跳转时拼接URL参数:
// ButtonMultiplayer的点击事件 navigate(`/game/${pickIndex}/vsPlayer`); - Board组件通过
useParams获取参数:import { useParams } from 'react-router-dom'; export default function Board() { const { pickIndex, gameMode } = useParams(); // ... 其他逻辑 }
- 修改路由配置,添加参数占位符:
无效尝试的原因
- 直接在条件语句中调用
useLocation违反React Hooks规则:Hooks必须在组件顶层调用,不能嵌套在条件、循环等语句内 sessionStorage未生效:大概率是未在选择pickIndex的时机存入缓存,或在Board组件中读取缓存的逻辑顺序错误
内容的提问来源于stack exchange,提问作者tomvas
相关产品推荐
相关产品推荐

