Keannylen
Keannylen

Reputation: 483

useState does not update in custom hooks for initial values

I am writing my own useFormControl hook and hit a problem that useState() not updating with initial values inside useEffect(), my hook as below

export default function useFormControl(
    initialValues: { [key: string]: unknown },
    validator: { [key: string]: (value: any) => boolean },
    errorText: { [key: string]: string }
): IUseFormReturn {
    const [errors, updateErrors] = useState<{ [key: string]: string }>({});

    const handleInputValue = (fieldName: string, value: unknown): void => {
        if (!has(validator, fieldName)) {
            return
        }
        
        if (!validator[fieldName](value)) {
            errors[fieldName] = has(errorText, fieldName) ? errorText[fieldName] : "Invalid Input"
            updateErrors(errors)
        } else {
            if (has(errors, fieldName)) {
                delete errors[fieldName]
                updateErrors(errors)
            }
        }
    }

    const initialValueStr = JSON.stringify(initialValues);

    useEffect(() => {
        for (const [fieldName, value] of Object.entries(initialValues)) {
            handleInputValue("X_FIELD", initialValues.X_FIELD);
        }
    }, [initialValueStr])

    return { handleInputValue, errors }
}

And where this hook been used as below

const initialValue = {
    X_FIELD: transactionTable.X_FIELD,
    ...
    <other_fields>
    ...
  }
  const { handleInputValue, errors } = useFormControl(
    initialValue,
    {
      X_FIELD: (value: string) => value.length === 7,
      ...
      <other_fields>
      ...
    },
    {
      X_FIELD: "Please enter correct X FIELD value",
      ...
      <other_fields>
      ...
    }
  )

handleInputValue() works as expected once user change form value and the corresponding error been added into errors and return properly from useFormControl().

The problem is the case which initialValues containing invalid X_FIELD value, for example I have X_FILED: "abce" inside initialValues which should be picked up by validator(requiring X_FIELD value with length of 7 only) and returned errors should contain X_FIELD: "Please enter correct X FIELD value" but I got errors = {} instead. I debug and can see errors has X_FIELD: "Please enter correct X FIELD value" but not returned.

Need help to identify why and how to fix this problem.

Upvotes: 1

Views: 177

Answers (1)

Drew Reese
Drew Reese

Reputation: 202618

I was about to take a pass on this one but took another look through your custom hook code and believe I've spotted the issue. The handleInputValue function is mutating the errors state object, and it never creates a new errors object reference.

const handleInputValue = (fieldName: string, value: unknown): void => {
  if (!has(validator, fieldName)) {
    return;
  }
    
  if (!validator[fieldName](value)) {
    errors[fieldName] = has(errorText, fieldName) // <-- Mutation!!
      ? errorText[fieldName]
      : "Invalid Input";
    updateErrors(errors); // <-- current errors state reference!
  } else {
    if (has(errors, fieldName)) {
      delete errors[fieldName]; // <-- Mutation!!
      updateErrors(errors); // <-- current errors state reference!
    }
  }
};

Compounding this issue is the fact that you are calling handleInputValue in a loop and using regular state updates. When you do this the enqueued state updates in the loop tend to overwrite any previously enqueued state update, so only the enqueued update from the last iteration updates state for the next render cycle.

Use functional state updates to correctly update state from any previous state value instead of the state value closed over in callback scope. Don't mutate state, always create shallow copies of state that is being udpated.

const handleInputValue = (fieldName: string, value: unknown): void => {
  if (validator[fieldName]) {
    updateErrors(errors => {
      if (!validator[fieldName](value)) {
        // Create new errors object, update property, return new object
        return {
          ...errors,
          [fieldName]: errorText[fieldName] ?? "Invalid Input"
        };
      } else if (errors[fieldName]) {
        // Create new errors object, delete property, return new object
        const newErrors = { ...errors };
        delete newErrors[fieldName];
        return newErrors;
      } else {
        // Return current state
        return errors;
      }
    });
  }
}

Upvotes: 2

Related Questions