Adding a key-value pair to an object inside loop using React useState hook

问题: I'm trying to add a key-value pair to an object using useState inside map function. However, the spread operation in setContainer({...container, [data.pk]: data.name}); see...

问题:

I'm trying to add a key-value pair to an object using useState inside map function. However, the spread operation in setContainer({...container, [data.pk]: data.name}); seems to be ineffective.

The code snippet below exactly illustrates the problem that I'm trying to solve. In fact, in my original code container is declared at another ContextProvider component and referenced using useContext, but I replaced it with useState for simpliticy.

I guess it has to do with the asynchronous nature of setState hook, but I don't fully understand what's going on under the hood.

Please let me know what is causing this issue and how to yield the outcome I expected. Thank you.

const {useState, useEffect} = React;

const myData = [{pk: 1, name:'apple'},
                {pk: 2, name:'banana'},
                {pk: 3, name:'citrus'},
]

const Example = () => {
  const [container, setContainer] = useState({});
  
  useEffect(()=>{
      myData.map(data=>{
        console.log(`During this iteration, a key-value pair (key ${data.pk} and value '${data.name}') should be added to container`);
            setContainer({...container, [data.pk]: data.name});
      })
  }, [])

  return (
    <div>
      <div>result I expected: {"{'1': 'apple', '2': 'banana','3': 'citrus'}"}</div>
      <div>result: {JSON.stringify(container)}</div>
    </div>
  );
};

// Render it
ReactDOM.render(
  <Example />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>


回答1:

Here's what's going on in your component step by step.

  1. container and setContainer are initialized using the useState hook
  2. the effect in which you map over myData is registered, also it will only run once in the entire life of the component because of the empty array [] you passed as the second argument
  3. the callback you registered in the effect is run. this is what it could look like if you console.log'd the container as well as the key-value pair
(key 1 value 'apple'); container {}
(key 2 value 'banana'); container {}
(key 3 value 'citrus'); container {}

Wait what, shouldn't container be updating on each iteration?

Well yes, but not in the way you expect. The container referenced in useEffect retains its original {} value in each iteration of myData.map. What's effectively going on then is

setContainer({ ...{}, 1: 'apple ' }) // RERENDER
setContainer({ ...{}, 2: 'banana' }) // RERENDER
setContainer({ ...{}, 3: 'citrus' }) // RERENDER

citrus is the final display since it was the last rerender.

Here's a way you can achieve your desired result

const {useState, useEffect} = React;

const myData = [{pk: 1, name:'apple'},
                {pk: 2, name:'banana'},
                {pk: 3, name:'citrus'},
]

const Example = () => {
  const [container, setContainer] = useState({});
  
  useEffect(()=>{
      setContainer(myData.reduce((obj, data) => ({ ...obj, [data.pk]: data.name }), {}))
  }, [])

  return (
    <div>
      <div>result I expected: {"{'1': 'apple', '2': 'banana','3': 'citrus'}"}</div>
      <div>result: {JSON.stringify(container)}</div>
    </div>
  );
};

// Render it
ReactDOM.render(
  <Example />,
  document.getElementById("react")
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="react"></div>


回答2:

You're right, it's because of asynchronous nature of setState hook, it basically runs in batch.

From react docs:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

You can create an object first and set it once the loop is completed. Also you shouldn't use map here. Since you're not creating a new array.

  useEffect(() => {
    const dataObj = {};
    myData.forEach(data => {
      dataObj[data.pk] = data.name;
    });
    setContainer(dataObj);
  }, []);

  • 发表于 2020-06-27 22:54
  • 阅读 ( 361 )
  • 分类:sof

条评论

请先 登录 后评论
不写代码的码农
小编

篇文章

作家榜 »

  1. 小编 文章
返回顶部
部分文章转自于网络,若有侵权请联系我们删除