Home > OS >  React Memo still rerenders on state update
React Memo still rerenders on state update

Time:06-16

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:

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.

  • Related