I am wondering why the console.log({ routes }) is outputting on a search useState update without the search having filtered the data. I have memoized and useCallbacked things.
import { memo, useCallback, useMemo, 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'
const StationComponent = (fetchSchedule, s) => {
const { id, location, name, routes = false, stops } = s
console.log({ routes }) // THIS RERENDERS
return (
<div key={id} 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></br>{new Date(time).toLocaleTimeString()}</li>)}</ul></div>}
{routes && routes.N.length > 0 && <div><label>South</label><ul>{routes.S.map(({ route, time }, idx) => <li key={idx}>Route: {route}<br></br>{new Date(time).toLocaleTimeString()}</li>)}</ul></div>}
</div>
)
}
const MemoizedStationComponent = memo(StationComponent)
function App() {
const [filteredStationData, setFilteredStationData] = useState(stations)
const [search, setSearch] = useState() // station name
const [stationData, setStationData] = useState(stations)
console.log({stations})
const fetchSchedule = useCallback(async (id) => {
const res = await fetch(`${baseUrl}/${id}`, { method: 'get' })
const json = await res.json()
console.log("apiCall")
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)
})
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}>
{Object.values(filteredStationData).map(s => <MemoizedStationComponent fetchSchedule={fetchSchedule} s={s} />)}
</div>
</header>
</div>
);
}
export default App;
CodePudding user response:
Few points:
- You haven't provided dependencies to
useCallback
.
Pass an inline callback and an array of dependencies. useCallback will return a memoized version of the callback that only changes if one of the dependencies has changed. This is useful when passing callbacks to optimized child components that rely on reference equality to prevent unnecessary renders (e.g. shouldComponentUpdate).
Also I notice you haven't passed keys here:
<MemoizedStationComponent fetchSchedule={fetchSchedule} s={s} />
; this can become problematic (you should have received warning), and you can end up in corrupt state etc., if the elements of the array which you are mapping can reorder for example.And general note, if you pass an object as prop to memoized component, make sure it is not recreated on each render, or it will break memoization.
CodePudding user response:
Just add dependencies for useCallback to prevent unnecessary re-renders of your station component.
const fetchSchedule = useCallback(async (id) => {
const res = await fetch(`${baseUrl}/${id}`, { method: 'get' })
const json = await res.json()
console.log("apiCall")
const copyStationData = JSON.parse(JSON.stringify(stations))
copyStationData[id].routes = json.data[0]
setStationData(copyStationData)
}, [stations])
const filterStations = useCallback(() => {
const filteredStations = Object.values(stationData).filter((s) => s.name.includes(search))
setFilteredStationData(filteredStations)
}, [search])
Now each time your stations or search value is changed. Your component will re-render.