skip to Main Content

Technical question here. I created a context provider to handle the state of some variables and to create functions to update these variables from other components. It is of my understanding that StricMode calls setStates twice, however, how are we supposed to handle pushing/popping elements from data arrays within states without duplicating these actions.

This is my context.jsx:

export const BrollProvider = ({ children }) => {
  //These are the states in question
  const [bRollItems, setbRollItems] = useState([]);
  const [timelineData, setTimelineData] = useState([
    {
        id: "0",
        actions: []
    },
  ]);
  
  //This is the function that upon being called updates the states
  const addBrollItemAndAction = (event, currentFrame) => {
    //Some irrelevant variables
    event.preventDefault();
    const bRollItemId = event.dataTransfer.getData('bRollItemId');
    const newAction = {
        id: bRollItemId,
        start: currentFrame / 30,
        end: (currentFrame / 30) + 3,
        isSelected: true,
        effectId: ''
      };
      
    //Next we have the setStates, regardless of the logic within them, they always get called twice
      
    //First setState, doesn't matter if it's called twice since it works on the same object everytime (id === id)
    setbRollItems((prevItems) =>
        prevItems.map((item) =>
            item.id === bRollItemId ? 
                { ...item, 
                    addedToTimeline: true, 
                    startFrame: currentFrame, durationInFrames: 90,
                    width:'85%',
                    transform: 'translate(32px, 37px)'
                } : item
        )
    );

    //This setState adds data to the existing timelineData array, so everytime we call it, 2 identical objects are .pushed to the data array.
    setTimelineData((prevTimelineData) => {
      const updatedData = [...prevTimelineData];
      const lastRow = updatedData[updatedData.length - 1];

      if (lastRow && lastRow.actions.length === 0) {
        lastRow.actions.push(newAction);
      } else {
        const newRow = {
          id: (parseInt(lastRow.id) + 1).toString(),
          actions: [newAction]
        };
        updatedData.push(newRow);
      }
      return updatedData;
    });

  };
  

  return (
    <BrollContext.Provider value={{ bRollItems, setbRollItems, timelineData, setTimelineData, addBrollItemAndAction }}>
      {children}
    </BrollContext.Provider>
  );
};

Now the addBrollItemAndAction function is called from another component (simplified for clarity):

const MyComponent = ({ propss }) => {
  const { bRollItems, setbRollItems, timelineData, setTimelineData, addBrollItemAndAction } = useMyCustomContextHook(); 
  const currentFrame = someIntValue
  
  const handleBrollDrop = (event) => {
        addBrollItemAndAction(event, currentFrame)
    }
    
  return(
    <div onDrag={handleBrollDrop}>
      <MyOtherComponent/>
    </div>
  )
}

When I disable StrictMode the issue is pretty much fixed, but not the underlying cause. With that being said, what is the best practice to modify arrays of data in developer mode? Specifically with .push() if you don’t want the same variable being pushed twice to the same state. And how come updating an array with .push() may cause bugs in the future? Seems pretty straightforward to me. Are we supposed to do workarounds or is this a measure taken by strictMode so we don’t use useState for what I’m intending to use it for?

2

Answers


  1. Chosen as BEST ANSWER

    What ended up fixing the issue was creating a deep copy of the state object array so instead of:

    const updatedData = [...prevTimelineData];
    

    This still holds a reference to the original state which causes direct mutations which is a no-no in React

    The right way to clone the state is:

    const updatedData = prevTimelineData.map(row => ({ ...row }));
    

    If I understand correctly, this creates a whole different copy of the state and doesnt affect it directly.


  2. First, It’s really not a good idea to put setState in the root of a component, even if it’s a provider. What should be done is to put the useState in

    useEffect(()=>{
    setState(....)
    },[])
    

    This will ensure that the setStatae will run once on the component mount phase (initialization).
    for better information you need to check React Lifecycle Methods where it explains how the component is mounted and unmounted. However, this article explains the states of the component on a class component but you can easily search for the equivalente methods for functional components.
    If you could not find these methods, just let me know.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search