You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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;
问题分析与解决方案

错误原因

  1. useFetch钩子未响应URL变化:useEffect依赖数组为空,仅在组件挂载时请求一次;reFetch方法使用的是初始传入的URL,后续state更新后不会同步最新参数。
  2. location.state可能为null:若直接访问List页面(未从其他页面携带state跳转),location.state会是null,初始化destination等state时直接访问location.state.destination会报错。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 16:22:17