简体   繁体   中英

Unexpected behavior of useState react hook at initial render

I am building a simple app that shows data using d3 graph by searched input value.

export const Search = () => {
  const [country, setCountry] = React.useState('');
  const [weatherData, setWeatherData] = React.useState([]);

  const changeText = e => {
    setCountry(e.target.value)
  }

  const searchCountry = () => {
    fetch(`https://apiweatherhistory/dayone/country/${country}`).then(res => res.json().then(data => {
      setWeatherData(weatherData)
    }))
  }

  const svg = d3.select('.weatherSearch')
    .append('svg')
    .attr('width', 960)
    .attr('height', 500)
    .style('background', 'black')

  return (
    <div>
      <input type='text' onChange={(e) => changeText(e)} value={country} />
      <button onClick={searchCountry}>Search</button>
      <div className='weatherSearch' />
    </div>
  )
}

When the component is mounted, d3 appends svg element on div tag name weatherSearch .

Problem is the component renders twice therfore svg tag is drawn twice as well. But when I comment out two states initialization,

  const [country, setCountry] = React.useState('');
  const [weatherData, setWeatherData] = React.useState([]);

the component renders only once and draw svg tag one time.

input or click interaction did not happen yet which means two state change methods (setCountry, setWeatherData) are not even used.

and Search component is the root component of index.js so there is no way parent component triggers rerendering.

Why is this happening? Only initialize states could trigger rerendering?

I suspect it is because appending the svg is in the functional component body so it will get executed every time the component is "rendered". By "render" I mean, when the react framework renders the virtualDOM tree to compute diffs during the "render" phase. This is different from the "commit" phase when the computed DOM is flushed to the actual DOM and when effects and others lifecycle functions run. The "render" phase can be paused, aborted, and restarted by react nearly any number of times.

在此处输入图像描述

I would place this logic in a useEffect hook with an empty dependency array so it runs on component mount.

export const Search = () => {
  const [country, setCountry] = React.useState('');
  const [weatherData, setWeatherData] = React.useState([]);

  const changeText = e => {
    setCountry(e.target.value)
  }

  const searchCountry = () => {
    fetch(`https://apiweatherhistory/dayone/country/${country}`).then(res => res.json().then(data => {
      setWeatherData(weatherData)
    }))
  }

  useEffect(() => {
    const svg = d3.select('.weatherSearch')
      .append('svg')
      .attr('width', 960)
      .attr('height', 500)
      .style('background', 'black');
  }, []);

  return (
    <div>
      <input type='text' onChange={(e) => changeText(e)} value={country} />
      <button onClick={searchCountry}>Search</button>
      <div className='weatherSearch' />
    </div>
  )
}

You can not say for sure how many times the component will be rerendered. It is not a deterministic algorithm, it's heuristic.

Also, it is not a good idea to mix up controlled virtual DOM with uncontrolled D3. Please look at this hook https://en.reactjs.org/docs/hooks-reference.html#uselayouteffect and this API https://en.reactjs.org/docs/portals.html to understand how to implement such cases.

But IMHO it is better to make SVG part controlled by React without D3. You can use SVG tags directly in virtual DOM.

Additional tips:

Handlers could be cached by useCallback . For example:

const onChange = useCallback(
  ({ target: { value } }) => setCountry(value),
  [setCountry],
);

<input onChange={onChangeText} />   

Also, it is better to name handlers onSomeEvent

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM