React Memo包裹组件在search状态更新时仍意外重渲染问题
React输入框更新触发子组件意外重渲染问题修复
核心问题原因
你做的memo和useCallback优化完全没生效,输入内容就触发子组件打印,是几个基础写法错误导致的:
- 组件入参写法不符合React规则:React函数组件只会接收1个参数,即所有传入属性组成的props对象。你写的
const StationComponent = (fetchSchedule, s) => {}是完全错误的,这种写法下第一个形参拿到的是整个props对象{ fetchSchedule: 函数, s: 站点数据 },第二个形参永远是undefined:一方面会导致点击站点时抛出"fetchSchedule is not a function"的错误,另一方面memo做浅比较时无法正确识别props是否变化,缓存直接失效,只要父组件重渲染子组件就会跟着重跑。 - useCallback缺失依赖项:你包裹
filterStations的useCallback没有传入依赖数组,也没有把内部用到的stationData、search加入依赖,会触发闭包陷阱,点击搜索时拿不到最新输入值。 - 附带两个逻辑bug:一是南向车次列表的判断条件错写为
routes.N.length > 0,会导致南向列表永远不渲染;二是列表渲染时key属性错误写在了子组件内部的div上,会触发React渲染警告。
修复方案
- 修正子组件的入参写法,直接在形参位置解构需要的props
- 给所有useCallback补全正确的依赖数组
- 修正南向车次的判断条件,把key属性移到map循环的组件上
- 给search状态设置空字符串初始值,避免调用
includes时触发类型错误
修复后的完整代码如下:
import { memo, useCallback, useState } from 'react' import * as stations from './stations.json' import './App.css'; const StyledContainer = { "cursor": "pointer", "display": "flex", "flex-direction": "row" } const StyledSegment = { "justify-content": "space-between", "text-align": "left", "width": "33.3%" } const baseUrl = 'https://api.wheresthefuckingtrain.com/by-id' // 正确解构props,直接用memo包裹组件 const StationComponent = memo(({ fetchSchedule, s }) => { const { id, location, name, routes = false } = s console.log({ routes }) // 仅在对应props变化时才会打印 return ( <div onClick={() => { fetchSchedule(id) }}> <h3>{name}</h3> <h4>{location}</h4> {routes && routes.N.length > 0 && <div><label>North</label><ul>{routes.N.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br />{new Date(time).toLocaleTimeString()}</li>)}</ul></div>} {/* 修正南向列表判断条件 */} {routes && routes.S.length > 0 && <div><label>South</label><ul>{routes.S.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br />{new Date(time).toLocaleTimeString()}</li>)}</ul></div>} </div> ) }) function App() { const [filteredStationData, setFilteredStationData] = useState(stations) const [search, setSearch] = useState('') const [stationData, setStationData] = useState(stations) const fetchSchedule = useCallback(async (id) => { const res = await fetch(`${baseUrl}/${id}`, { method: 'get' }) const json = await res.json() const copyStationData = JSON.parse(JSON.stringify(stations)) copyStationData[id].routes = json.data[0] setStationData(copyStationData) }, []) // 无外部依赖,依赖数组为空即可 const filterStations = useCallback(() => { const filteredStations = Object.values(stationData).filter((s) => s.name.includes(search)) setFilteredStationData(filteredStations) }, [stationData, search]) // 补全依赖项 return ( <div className="App" style={StyledContainer}> <header className="App-header"> <input placeholder="Name" onChange={(e) => { setSearch(e.target.value) }} /> <button onClick={filterStations}>Search</button> <div style={StyledSegment}> {/* 把key属性移到循环的组件上 */} {Object.values(filteredStationData).map(s => <StationComponent key={s.id} fetchSchedule={fetchSchedule} s={s} />)} </div> </header> </div> ); } export default App;
修复后,只有当你点击搜索按钮更新filteredStationData、或者拉取班次更新对应站点的routes数据时,对应StationComponent才会重渲染,输入框修改search值不会再触发子组件的console输出。
内容的提问来源于stack exchange,提问作者Michael Paccione
相关产品推荐
相关产品推荐

