" /> " /> "/>

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

32 views Asked by At
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?

1

There are 1 answers

2
Drew Reese On BEST ANSWER

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>
  );
}