React点击分类组件报destination读取null错误的原因排查
问题描述
我开发了一个React组件List,内部包含Places组件,通过URL获取的数据作为props传入来展示不同地点。URL支持destination、min、max和type查询参数。在swiper-container内的react-router-dom的Link组件中创建了handleType方法,点击图标时该方法接收类型参数并重新获取对应类型的数据,但点击后出现错误:"Cannot read properties of null (reading 'destination')",即使已为URL中的destination设置默认值,问题仍存在。
附上List组件代码:
import NavBar from "../../components/navbar/Navbar"; import Header from "../../components/header/Header"; import { Link, useLocation } from "react-router-dom"; import { useState } from "react"; import { format } from "date-fns"; import { DateRange } from "react-date-range"; import SearchItems from "../propertyList/SearchItems"; import Places from "../propertyList/Places"; import { faBuilding, faCampground, faCircleArrowLeft, faCircleArrowRight, faFilter, faHotel, faHouse, faSwimmingPool, } from "@fortawesome/free-solid-svg-icons"; import { Button } from "react-bootstrap"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Swiper, SwiperSlide } from "swiper/react"; import { Navigation, Pagination, Scrollbar, A11y } from "swiper"; import useFetch from "../../hooks/useFetch"; import "swiper/css"; const List = () => { const location = useLocation(); const [destination, setDestination] = useState(location.state.destination); const [date, setDate] = useState(location.state.date); const [openDate, setOpenDate] = useState(false); const [options, setOptions] = useState(location.state.options); const [openList, setOpenList] = useState(false); const [min, setMin] = useState(undefined); const [max, setMax] = useState(); const [type, setType] = useState(null); const baseURL = import.meta.env.VITE_REACT_API_URL; const { data, loading, error, reFetch } = useFetch( baseURL + `/hotels?city=${destination || ""}&min=${min || 0}&max=${max || 999}&type=${type || ""}` ); const handleClick = () => { reFetch(); }; const handleType = (categoryType) => { console.log(categoryType); setType(categoryType); reFetch(); } const categories = [ { id: 1, name: "house", icon: <FontAwesomeIcon icon={faHouse} />, }, { id: 2, name: "pool", icon: <FontAwesomeIcon icon={faSwimmingPool} />, }, { id: 3, name: "camp", icon: <FontAwesomeIcon icon={faCampground} />, }, { id: 4, name: "apartment", icon: <FontAwesomeIcon icon={faBuilding} />, }, { id: 5, name: "hotel", icon: <FontAwesomeIcon icon={faHotel} />, }, ]; return ( <div> <NavBar /> <div className="categories"> <div className="swiper-button-prev"> <FontAwesomeIcon icon={faCircleArrowLeft} /> </div> <Swiper modules={[Navigation]} spaceBetween={50} slidesPerView={3} navigation={{ nextEl: ".swiper-button-next", prevEl: ".swiper-button-prev", }} > {categories.map((cat,i) => ( <SwiperSlide> <div className="swiper-container"> <Link onClick={()=>handleType(cat.name)}> <h2>{cat.icon}</h2> <span>{cat.name}</span> </Link> </div> </SwiperSlide> ))} </Swiper> <div className="swiper-button-next"> <FontAwesomeIcon icon={faCircleArrowRight} style={{ color: "#000000" }} /> </div> <Button onClick={() => setOpenList(!openList)}> <FontAwesomeIcon icon={faFilter} /> <span>Filters</span> </Button> </div> <div className="listContainer"> <div className="listWrapper"> {openList && ( <div className="listSearch"> <h1 className="lsTitle">Search</h1> <div className="lsItem"> <label>Destination</label> <input placeholder={destination} type="text" /> </div> <div className="lsItem"> <label>Check-in Date</label> <span onClick={() => setOpenDate(!openDate)}>{`${format( date[0].startDate, "MM/dd/yyyy" )} to ${format(date[0].endDate, "MM/dd/yyyy")}`}</span> {openDate && ( <DateRange onChange={(item) => setDate([item.selection])} minDate={new Date()} ranges={date} /> )} </div> <div className="lsItem"> <label>Options</label> <div className="lsOptions"> <div className="lsOptionItem"> <span className="lsOptionText"> Min price <small>per night</small> </span> <input type="number" onChange={(e) => setMin(e.target.value)} className="lsOptionInput" /> </div> <div className="lsOptionItem"> <span className="lsOptionText"> Max price <small>per night</small> </span> <input type="number" onChange={(e) => setMax(e.target.value)} className="lsOptionInput" /> </div> <div className="lsOptionItem"> <span className="lsOptionText">Adult</span> <input type="number" min={1} className="lsOptionInput" placeholder={options.adult} /> </div> <div className="lsOptionItem"> <span className="lsOptionText">Children</span> <input type="number" min={0} className="lsOptionInput" placeholder={options.children} /> </div> <div className="lsOptionItem"> <span className="lsOptionText">Room</span> <input type="number" min={1} className="lsOptionInput" placeholder={options.rooms} /> </div> </div> </div> <button onClick={handleClick}>Search</button> </div> )} <div className="listResult"> <Places items={data} /> </div> </div> </div> </div> ); }; export default List;
useFetch钩子代码:
import { useEffect, useState } from "react"; import axios from "axios"; const useFetch = (url) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); try { const res = await axios.get(url); setData(res.data); } catch (error) { setError(error); } setLoading(false); }; fetchData(); }, []); const reFetch = async () => { setLoading(true); try { const res = await axios.get(url); setData(res.data); } catch (error) { setError(error); } setLoading(false); }; return { data, loading, error, reFetch }; }; export default useFetch;
问题分析与解决方案
错误原因
- useFetch钩子未响应URL变化:useEffect依赖数组为空,仅在组件挂载时请求一次;reFetch方法使用的是初始传入的URL,后续state更新后不会同步最新参数。
- location.state可能为null:若直接访问List页面(未从其他页面携带state跳转),
location.state会是null,初始化destination等state时直接访问location.state.destination会报错。 - Link组件的默认跳转行为:点击Link会触发路由跳转,导致页面重新加载,
location.state丢失,进而引发destination读取错误。
修复步骤
1. 修复useFetch钩子,使其响应URL变化
修改useFetch的useEffect依赖为[url],确保URL变化时自动重新请求;同时reFetch方法会使用最新的URL:
import { useEffect, useState } from "react"; import axios from "axios"; const useFetch = (url) => { const [data, setData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(false); useEffect(() => { const fetchData = async () => { setLoading(true); try { const res = await axios.get(url); setData(res.data); } catch (error) { setError(error); } setLoading(false); }; fetchData(); }, [url]); // 添加url作为依赖,响应url变化 const reFetch = async () => { setLoading(true); try { const res = await axios.get(url); setData(res.data); } catch (error) { setError(error); } setLoading(false); }; return { data, loading, error, reFetch }; }; export default useFetch;
2. 为location.state设置默认值,避免null报错
初始化state前先判断location.state是否存在,提供默认值:
const location = useLocation(); // 给location.state设置默认值,防止直接访问null的属性 const initialState = location.state || { destination: "", date: [{ startDate: new Date(), endDate: new Date() }], options: { adult: 1, children: 0, rooms: 1 } }; const [destination, setDestination] = useState(initialState.destination); const [date, setDate] = useState(initialState.date); const [options, setOptions] = useState(initialState.options);
3. 阻止Link的默认跳转行为
此处Link仅用于触发点击事件,无需路由跳转,添加e.preventDefault()阻止默认行为:
<Link onClick={(e) => { e.preventDefault(); // 阻止Link默认跳转 handleType(cat.name); }}> <h2>{cat.icon}</h2> <span>{cat.name}</span> </Link>
4. 优化请求时机,确保使用最新state
setType是异步操作,直接调用reFetch可能拿到旧的type值,可通过useEffect监听所有影响URL的state变化,自动触发请求:
import { useEffect } from "react"; // 确保导入useEffect // 在组件内添加以下代码 useEffect(() => { reFetch(); }, [type, min, max, destination]); // 监听所有影响URL参数的state
这样修改后,点击分类图标时会正确更新type参数,重新请求对应数据,同时避免了location.state为null的报错。
内容的提问来源于stack exchange,提问作者Mohammed Hamdoon
相关产品推荐
相关产品推荐

