Jack Zhang
Jack Zhang

Reputation: 2796

Edit object in array directly in React is working but use hooks is not

import { useState } from "react";

type Rule = {
  value: string;
  valid: boolean;
};

type RuleComponentProps = {
  rule: Rule;
  setValue: (value: string) => void;
  setValid: (valid: boolean) => void;
};

function RuleComponent({ rule, setValue, setValid }: RuleComponentProps) {
  function validator(value: string) {
    rule.valid = !!value;

    // Why below line doesn't work?
    // setValid(!!value);
  }

  function handleChange(e: any) {
    const value = e.target.value;
    validator(value);
    setValue(value);
    // rule.value = value;
  }

  return <input onChange={handleChange} value={rule.value} />;
}

export default function App() {
  const [ruleList, setRuleList] = useState<Rule[]>([]);

  function addRule() {
    setRuleList((prev) => [...prev, { value: "", valid: false }]);
  }

  function getSetValueMeth(index: number) {
    return (value: string) => {
      const newRuleList = ruleList.map((rule, i) => {
        if (index === i) {
          return { ...rule, value };
        }
        return rule;
      });
      setRuleList(newRuleList);
    };
  }

  function getSetValidMeth(index: number) {
    return (valid: boolean) => {
      const newRuleList = ruleList.map((rule, i) => {
        if (index === i) {
          return { ...rule, valid };
        }
        return rule;
      });
      setRuleList(newRuleList);
    };
  }

  return (
    <div className="App">
      <div>
        <button onClick={() => addRule()}>Add rule</button>
      </div>
      {ruleList.map((rule, index) => (
        <p key={index}>
          Rule{index}:{" "}
          <RuleComponent
            rule={rule}
            setValue={getSetValueMeth(index)}
            setValid={getSetValidMeth(index)}
          />
        </p>
      ))}
      <p>{JSON.stringify(ruleList)}</p>
      <button onClick={() => console.log(ruleList)}>Show Rule List</button>
    </div>
  );
}

I'm trying to edit the valid value in rulelist in above code. It's working when using rule.valid = !!value;, but when I change to setValid(!!value); method, the valid value didn't change even the setRuleList hooks called and with right updated newRuleList.

I created a CodeSandbox for this: https://codesandbox.io/p/sandbox/react-demo-dfs236?file=%2Fsrc%2FApp.tsx%3A17%2C29, you can debug it directly.

I know it's not proper to edit value directly in React, but why the hook not work? What's the proper way to accomplish my feature?

Upvotes: 1

Views: 38

Answers (1)

Drew Reese
Drew Reese

Reputation: 203373

Issue

The enqueued state update by the setValid call in validator (getSetValidMeth) is wiped out by the enqueued state update by the setValue call in handleChange since each has a closure over the un-updated ruleList state value at the time handleChange is called.

Using rule.valid = !!value; was mutating the current state reference so it persisted through to the next render cycle.

Solution

Use a functional state update so each enqueued update correctly updates from any previous state value instead of whatever is closed over in callback scope.

const getSetValueMeth = (index: number) => (value: string) =>
  setRuleList((ruleList) =>
    ruleList.map((rule, i) => (index === i ? { ...rule, value } : rule))
  );

const getSetValidMeth = (index: number) => (valid: boolean) =>
  setRuleList((ruleList) =>
    ruleList.map((rule, i) => (index === i ? { ...rule, valid } : rule))
  );

Full code:

function RuleComponent({ rule, setValue, setValid }: RuleComponentProps) {
  function validator(value: string) {
    setValid(!!value);
  }

  function handleChange(e: any) {
    const value = e.target.value;
    validator(value);
    setValue(value);
  }

  return <input onChange={handleChange} value={rule.value} />;
}

export default function App() {
  const [ruleList, setRuleList] = useState<Rule[]>([]);

  function addRule() {
    setRuleList((prev) => [...prev, { value: "", valid: false }]);
  }

  const getSetValueMeth = (index: number) => (value: string) =>
    setRuleList((ruleList) =>
      ruleList.map((rule, i) => (index === i ? { ...rule, value } : rule))
    );

  const getSetValidMeth = (index: number) => (valid: boolean) =>
    setRuleList((ruleList) =>
      ruleList.map((rule, i) => (index === i ? { ...rule, valid } : rule))
    );

  return (
    <div className="App">
      <div>
        <button onClick={() => addRule()}>Add rule</button>
      </div>
      {ruleList.map((rule, index) => (
        <p key={index}>
          Rule{index}:{" "}
          <RuleComponent
            rule={rule}
            setValue={getSetValueMeth(index)}
            setValid={getSetValidMeth(index)}
          />
        </p>
      ))}
      <p>{JSON.stringify(ruleList)}</p>
      <button onClick={() => console.log(ruleList)}>Show Rule List</button>
    </div>
  );
}

Upvotes: 0

Related Questions