3

I am struggling to get the real previous state of my inputs. I think the real issue Which I have figured out while writing this is my use of const inputsCopy = [...inputs] always thinking that this creates a deep copy and i won't mutate the original array.

const [inputs, setInputs] = useState(store.devices)

store.devices looks like this

devices = [{
 name: string,
network: string,
checked: boolean,
...etc
}]

I was trying to use a custom hook for getting the previous value after the inputs change. I am trying to check if the checked value has switched from true/false so i can not run my autosave feature in a useEffect hook.

function usePrevious<T>(value: T): T | undefined {
    // The ref object is a generic container whose current property is mutable ...
    // ... and can hold any value, similar to an instance property on a class
    const ref = useRef<T>();

    // Store current value in ref
    useEffect(() => {
        ref.current = value;
    }); // Only re-run if value changes

    // Return previous value (happens before update in useEffect above)
    return ref.current;
}

I have also tried another custom hook that works like useState but has a third return value for prev state. looked something like this.

const usePrevStateHook = (initial) => {
    const [target, setTarget] = useState(initial)
    const [prev, setPrev] = useState(initial)

    const setPrevValue = (value) => {
        if (target !== value){ // I converted them to JSON.stringify() for comparison
            setPrev(target)
            setTarget(value)
        }
    }
    return [prev, target, setPrevValue]

}

These hooks show the correct prevState after I grab data from the api but any input changes set prev state to the same prop values. I think my issue lies somewhere with mobx store.devices which i am setting the initial state to or I am having problems not copying/mutating the state somehow. I have also tried checking what the prevState is in the setState

setInputs(prev => {
    console.log(prev)
    return inputsCopy
})

After Writing this out I think my issue could be when a value changes on an input and onChange goes to my handleInputChange function I create a copy of the state inputs like

const inputsCopy = [...inputs]
inputsCopy[i][prop] = value
setInputs(inputsCopy)

For some reason I think this creates a deep copy all the time. I have had hella issues in the past doing this with redux and some other things thinking I am not mutating the original variable.

Cheers to all that reply!

EDIT: Clarification on why I am mutating (not what I intended) I have a lot of inputs in multiple components for configuring a device settings. The problem is how I setup my onChange functions <input type="text" value={input.propName} name="propName" onChange={(e) => onInputChange(e, index)} />

const onInputChange = (e, index) => {
    const value = e.target.value;
    const name = e.target.name;
    const inputsCopy = [...inputs]; // problem starts here
    inputsCopy[index][name] = value; // Mutated obj!?
    setInputs(inputsCopy);
}

that is What I think the source of why my custom prevState hooks are not working. Because I am mutating it.

my AUTOSAVE feature that I want to have the DIFF for to compare prevState with current

const renderCount = useRef(0)
useEffect(() => {
    renderCount.current += 1
    if (renderCount.current > 1) {
        let checked = false
       // loop through prevState and currentState for checked value
       // if prevState[i].checked !== currentState[i].checked checked = true

        if (!checked) {
            const autoSave = setTimeout(() => {
                // SAVE INPUT DATA TO API
            }, 3000)

            return () => {
               clearTimeout(autoSave)
            }
        }
    }
}, [inputs])

Sorry I had to type this all out from memory. Not at the office.

J. Cutshall
  • 302
  • 3
  • 13
  • 1
    `setState(prevState => { ... update logic ...})` should be all you need to access the previous state. Other than this it's difficult to understand what any issue is or what you're trying to do. Are you just trying to update state from the previous state value? `const inputsCopy = [...inputs]` is only a shallow copy. – Drew Reese Dec 10 '21 at 22:35
  • 1
    `useEffect` without the second parameter will execute it everytime, add `value` the the deps :) – Sysix Dec 10 '21 at 22:42
  • @DrewReese I want to have the previous state stored in a variable called `prevInputState` so my useEffect that runs on input change can determine which value has changed to see if I need to PUT the data with the api. I simply don't want to make a put request to my api if the prop checked has changed because I only use it locally on the front end to handle selecting a device for removal. Its for my simple AutoSave feature which starts a setTimeout on input change and clears the timeout if there is another render before timeout. As I thought was the problem. I am mutating my state onChange. – J. Cutshall Dec 10 '21 at 22:58
  • 1
    I see. The `usePrevious` custom hook should be sufficient enough to store the previous state value, to be used/referenced in an `useEffect` when the state updates so you can diff them to see what changed. Can you update your question to include these details about the use case so we can see how the diff is used? – Drew Reese Dec 10 '21 at 23:03
  • @DrewReese Added some more code under Edit section. – J. Cutshall Dec 10 '21 at 23:16

1 Answers1

1

If I understand your question, you are trying to update state from the previous state value and avoid mutations. const inputsCopy = [...inputs] is only a shallow copy of the array, so the elements still refer back to the previous array.

const inputsCopy = [...inputs] // <-- shallow copy
inputsCopy[i][prop] = value    // <-- this is a mutation of the current state!!
setInputs(inputsCopy)

Use a functional state update to access the previous state, and ensure all state, and nested state, is shallow copied in order to avoid the mutations. Use Array.prototype.map to make a shallow copy of the inputs array, using the iterated index to match the specific element you want to update, and then also use the Spread Syntax to make a shallow copy of that element object, then overwrite the [prop] property value.

setInputs(inputs => inputs.map(
  (el, index) => index === i
    ? {
      ...el,
      [prop] = value,
    }
    : el
);

Though this is a Redux doc, the Immutable Update Patterns documentation is a fantastic explanation and example.

Excerpt:

Updating Nested Objects

The key to updating nested data is that every level of nesting must be copied and updated appropriately. This is often a difficult concept for those learning Redux, and there are some specific problems that frequently occur when trying to update nested objects. These lead to accidental direct mutation, and should be avoided.

Drew Reese
  • 165,259
  • 14
  • 153
  • 181
  • Thank you for your answer! This does seem to be my issue! – J. Cutshall Dec 10 '21 at 23:16
  • 1
    @J.Cutshall Ah yes... if at step (1) you are mutating the state in `onInputChange` then you're already off on the wrong foot, so to say. One of the Commandments of React: "Thou shall not mutate state!". Mutating the state object can lead to odd side-effects/bugs that can be rather difficult to track oftentimes. So long as you are not mutating what is to be considered the previous state when computing the next state, then diffing against it can work as you're likely expecting it to. – Drew Reese Dec 10 '21 at 23:24