Reputation: 2796
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
Reputation: 203373
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.
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