J Seabolt
J Seabolt

Reputation: 2978

React hooks - not updating consistently

So I'm semi-new to hooks. I want to run some basic validation. Running into a strange issue: when I run two hooks back-to-back, only the second of two hooks works.

const [validationTracking, setValidationTracking] = useState({});

const setValidation = (idx, field, value) => {
  const validationCopy = cloneDeep(validationTracking);
  if (!validationCopy[idx]) {
    validationCopy[idx] = {};
  }
  validationCopy[idx][field] = value;

  setValidationTracking(validationCopy);
};

const validateInputs = () => {
  partnerInfo.forEach((object, idx) => {
    if (!object['title']) {
      setValidation(idx, 'title', true);
    }
    if (!object['body']) {
      setValidation(idx, 'body', true);
    }
  });
};

In the above code partnerInfo=[{title: '', body: ''}]

The validation only gets triggered for body when I run validateInputs

If the array has more than one item, only the very last field will get its validation set to true [{body: true}]

The input above SHOULD set validationTracking to [{title: true, body: true}]but it seems to skip or override earlier items

I know this.setState() in class-based components is async. I'm wondering if something similar is happening here..?

Upvotes: 2

Views: 1105

Answers (2)

ichigolas
ichigolas

Reputation: 7725

You need to understand that useState hook works in a functional way. When you call it, it triggers a re-render of the component, passing the new state value to it. State values are immutable, they are not references to values that can change. This is why we say that a React function components acts as pure functions with respect to their props.

So when you call setValidationTracking(validationCopy) twice during a single update, you send two state updates that are computed using the current state for this iteration.

I.e: when the second loop calls cloneDeep(validationTracking), validationTracking has not changed because the re-render triggered by the first loop has not happened and the state value is immutable any way.

To fix the problem you can instead pass a state updater function:

setValidationTracking(currentValidationTracking => ({
  ...currentValidationTracking,
  [idx]: {
    ...(currentValidationTracking[idx] || {}),
    [field]: value
  }
}));

Upvotes: 0

Dacre Denny
Dacre Denny

Reputation: 30360

There are a few things to be aware of with the useState hook:

  • the state change will not be immediately visible to your component logic until the next re-render of your component (and reevaluation of any closures)
  • if multiple calls are made to a state hook's "setter" in a single render cycle, only the last state update will be collected and applied in the subsequent render cycle

The second point is more relevant to your question, given the forEach iteration in your code makes multiple calls to the setValiadation setter of your state hook. Because these are made in a single render cycle, only the last call to setValiadation will have an observable effect.

The usual way to address this is to gather all state changes into a single object, and apply those with a single call to your setter. You could take the following approach to achieve that:

const [validationTracking, setValidationTracking] = useState({});

// Revised function
const updateValidation = (object, idx, field, value) => {
  const validationCopy = cloneDeep(object);
  if (!validationCopy[idx]) {
    validationCopy[idx] = {};
  }
  validationCopy[idx][field] = value;

  return validationCopy
};

const validateInputs = () => {

  // Call setter via a callback that transforms current state
  // into a new state object for the component
  setValidationTracking(state => {

    // Reduce partnerInfo array to a new state object
    return partnerInfo.reduce((acc, infoObject, idx) => {

      if (!infoObject['title']) {
        acc = updateValidation(acc, idx, 'title', true);
      }
      if (!infoObject['body']) {
        acc = updateValidation(acc, idx, 'body', true);
      }

      return acc;

    }, state);    

  });  
};

Hope that helps!

Upvotes: 1

Related Questions