javascript - 反应 setState 钩子不更新循环外
问题描述
更新:
这是一个代码框示例!
我有一个 textarea 组件,我在其中逐行执行验证:
UI 中的错误消息是一组对象,这些对象具有基于文本所在行的 id 属性和将容纳错误的 message 属性。e.g. {id: 1, message: 'error message', name: 'text from line'}
据我了解,您不能在循环中设置状态,因为不能保证结果。a
这是获取字符串的 setMessage 函数:
setMessage 在我的验证函数中被调用:
function setMessage(data) {
console.log('data', data);
console.log("arrFromVariableTypeNameString ", arrFromVariableTypeNameString);
let allMessages = [...messagesContainer];
function drop(data, func) {
var result = [];
for (var i = 0; i < data.length; i++) {
var check = func(data[i]);
console.log("check ", check);
if (check) {
console.log("i + 1 ", i + 1);
result = data.slice(i, i + 1);
break;
}
}
return result;
}
for (var i = 0; i < arrFromVariableTypeNameString.length; i++) {
var match = drop(allMessages, e => e.id === i + 1);
if (match?.length) {
match[0] = {
...match[0],
...{
message: data,
name: arrFromVariableTypeNameString[i]
}
}
console.log("match ", match);
console.log("allMessages ", allMessages);
allMessages = allMessages.map(t1 => ({
...t1,
...match.find(t2 => {
console.log("t2.id === t1.id ", t2.id === t1.id);
return t2.id === t1.id
})
}))
} else {
allMessages.push({
name: arrFromVariableTypeNameString[i],
id: i + 1,
message: data
})
}
}
setMessagesContainer(allMessages)
}
这是整个组件:
export function VariableSetupModal({
exisitingVariableTypes
}) {
const dispatch = useDispatch();
const [isOpen, setIsOpen] = useState();
const [variableTypeName, setVariableTypeName] = useState('');
const [clipboardData, setClipboardData] = useState('')
const [pasted, setIsPasted] = useState(false)
const [messages, setMessages] = useState('');
const [messagesContainer, setMessagesContainer] = useState([]);
var arrFromVariableTypeNameString = variableTypeName.split('\n');
useEffect(() => {
function setMessage(data) {
console.log('data', data);
console.log("arrFromVariableTypeNameString ", arrFromVariableTypeNameString);
let allMessages = [...messagesContainer];
function drop(data, func) {
var result = [];
for (var i = 0; i < data.length; i++) {
var check = func(data[i]);
console.log("check ", check);
if (check) {
console.log("i + 1 ", i + 1);
result = data.slice(i, i + 1);
break;
}
}
return result;
}
for (var i = 0; i < arrFromVariableTypeNameString.length; i++) {
var match = drop(allMessages, e => e.id === i + 1);
if (match ? .length) {
match[0] = { ...match[0],
...{
message: data,
name: arrFromVariableTypeNameString[i]
}
}
console.log("match ", match);
console.log("allMessages ", allMessages);
allMessages = allMessages.map(t1 => ({ ...t1,
...match.find(t2 => {
console.log("t2.id === t1.id ", t2.id === t1.id);
return t2.id === t1.id
})
}))
} else {
allMessages.push({
name: arrFromVariableTypeNameString[i],
id: i + 1,
message: data
})
}
}
setMessagesContainer(allMessages)
}
function validator(variableType) {
var data = {
variableType: variableType,
}
var rules = {
variableType: "regex:^[a-zA-Z0-9_ ]+$|min:3|max:20",
}
var messages = {
min: `Enter at least three characters.`,
max: `Don't exceed more than twenty characters.`,
regex: `No special characters (but spaces) allowed.`
}
validate(data, rules, messages)
.then(success => {
console.log('Variable Type Entered correctly.', success)
setMessage('');
return
})
.catch(error => {
console.log('error', error)
setMessage(error[0].message);
return
});
}
function checkIfArrayIsUnique(myArray) {
if (myArray.length === 50) setMessages('Only 50 Variable Types allowed.');
return myArray.length === new Set(myArray).size;
}
arrFromVariableTypeNameString.map((variableType, i, thisArr) => {
function findDuplicates(uniqueCount) {
var count = {},
result = '';
uniqueCount.forEach((i) => {
count[i] = (count[i] || 0) + 1;
});
console.log(count);
return Object.keys(count).map((k) => {
if (count[k] > 1) return result.concat(`Variable Type ${k}: appears ${count[k]} times.`)
}).filter((item) => item !== undefined)
}
if (checkIfArrayIsUnique(thisArr)) {
if (validator(variableType)) {
return thisArr;
}
} else {
setMessage(findDuplicates(thisArr).map(s => < > {
s
} < br / > < />));
return;
}
})
return () => {
setMessagesContainer([])
console.log("messagesContainer clean up ", messagesContainer);
}
}, [variableTypeName])
const handlePaster = (e) => {
e.persist()
setIsPasted(true);
setClipboardData(e.clipboardData.getData('text'));
}
const handleChange = (e) => {
e.persist()
var {
keyCode
} = e;
var {
value
} = e.target;
if (keyCode === 13) {
setVariableTypeName(`${value}\n`);
return;
} else if ((pasted == true) && (keyCode == 13)) {
setVariableTypeName(`${variableTypeName.concat(clipboardData)}\n`);
setIsPasted(false);
return;
} else if ((pasted == true) && (keyCode !== 13)) {
setVariableTypeName(`${variableTypeName.concat(clipboardData)}`);
setIsPasted(false);
return;
} else {
setVariableTypeName(`${value}`);
return;
}
}
return ( <
div >
<
Button className = "button"
onClick = {
evt => setIsOpen(true)
} > Add Variable Types < /Button> <
div style = {
{
display: "none"
}
} >
<
Modal id = "myModal"
heading = "Variable Type Configuration"
description = ""
userClosable = {
true
}
autoFocus = {
false
}
actionsLeft = { <
React.Fragment >
<
Button display = "text"
onClick = {
handleCancel
} > Cancel < /Button> <
Button display = "primary"
onClick = {
handleSave
} > Save < /Button> <
/React.Fragment>
}
isOpen = {
isOpen
}
onRequestClose = {
detail => {
handleCancel(false);
setMessagesContainer([])
}
} >
{
exisitngVarFormatted != "" && < Textbox
as = "textarea"
type = "text"
value = {
exisitngVarFormatted
}
disabled >
Existing <
/Textbox>}
<
Textbox
as = "textarea"
type = "text"
placeholder = "Variable Types"
maxLength = "100"
value = {
variableTypeName
}
onPaste = {
e => handlePaster(e)
}
onChange = {
e => handleChange(e)
} >
To Create <
/Textbox>
{
messagesContainer.map((messageObj, i, arr) => {
console.log("messageObj ", messageObj);
return messageObj.message != '' ? ( <
p key = {
messageObj.id
}
className = "Messages" > {
`Error on line ${i + 1}: ${messageObj.message}`
} < /p>
) : null
})
}
<
/Modal> <
/div> <
/div >
);
}
这就是让我把剩下的头发拉出来的原因。
在日志中,您可以清楚地看到对象设置正确,但在 UI 中,消息不是唯一的!70号线
任何帮助,将不胜感激!
解决方案
您更新单个消息的方法非常复杂。没必要那么难!这是一种通过索引不可变地更新数组的单个项目的方法:
const setMessageForLine = (message, lineNumber) => {
setMessagesContainer((existing) => [
...existing.slice(0, lineNumber),
message,
...existing.slice(lineNumber + 1)
]);
}
messagesContainer
我们使用回调符号来获取as的当前值existing
。如果更新快速连续完成并由 React 批处理,这可以防止更新相互干扰。
messagesContainer
我们可以通过保存ref
我们验证的最后一组行来减少我们需要做的更新量。如果文本与以前相同,则我们不需要再次验证它。但是我的实现存在一些错误。
我认为将每个错误分配给特定行是最有意义的。所以我正在更改“超过 50 行”和“重复”错误以应用于它们发生的行。在第一种情况下,我们只检查索引是否为>50
. 对于重复项,我们将文本与所有先前的元素进行比较。这意味着重复对的第一个条目即使稍后被复制也不会出错。现在我正在考虑这个问题,ref
如果有人要在较高行编辑现有项目,使其成为较低行的副本,这可能会带来一些问题,因为较低行不会被重新评估。
您正在使用async
验证库,因此每行都有同步和异步验证。您基于 aregex
和 length 进行的检查可以很容易地进行同步以使事情变得更简单。
使用当前的异步验证
import "./styles.css";
import React, { useEffect, useState, useRef, useCallback } from "react";
import { validate } from "indicative/validator";
export function TextArea({ onSave }) {
const [variableTypeName, setVariableTypeName] = useState("");
const [clipboardData, setClipboardData] = useState("");
const [pasted, setIsPasted] = useState(false);
const [messagesContainer, setMessagesContainer] = useState([]);
// don't need to validate the same text more than once
const lastCheckedLines = useRef([]);
const setMessageForLine = useCallback(
(message, lineNumber) => {
setMessagesContainer((existing) => [
...existing.slice(0, lineNumber),
message,
...existing.slice(lineNumber + 1)
]);
},
[setMessagesContainer]
);
const getLineError = useCallback(
(text, index, all) => {
// if too many lines
if (index >= 50) {
return "Only 50 Variable Types allowed.";
}
// blank lines will show up as duplicates of each other
if (text.length === 0) {
return "No empty lines";
}
// check if this line is the same as any of the previous
const duplicateOf = all.slice(0, index).findIndex((v) => v === text);
if (duplicateOf !== -1) {
return `Duplicate of line ${duplicateOf + 1}`;
}
},
[]
);
const asyncValidateLine = useCallback(
(text, index) => {
var data = {
variableType: text
};
var rules = {
variableType: "regex:^[a-zA-Z0-9_ ]+$|min:3|max:20"
};
var messages = {
min: `Enter at least three characters.`,
max: `Don't exceed more than twenty characters.`,
regex: `No special characters (but spaces) allowed.`
};
validate(data, rules, messages)
.then((success) => {
console.log("Variable Type Entered correctly.", success);
setMessageForLine("", index);
})
.catch((error) => {
console.log("error", error);
setMessageForLine(error[0].message, index);
});
},
[setMessageForLine]
);
useEffect(() => {
const lineTexts = variableTypeName.split("\n");
// remove extra lines when deleting
setMessagesContainer((existing) =>
existing.length > lineTexts.length
? existing.slice(0, lineTexts.length)
: existing
);
lineTexts.forEach((text, i) => {
// only check if we have a new text
if (text !== lastCheckedLines.current[i]) {
console.log(`evaluating line ${i + 1}`);
const error = getLineError(text, i, lineTexts);
if (error) {
setMessageForLine(error, i);
} else {
asyncValidateLine(text, i);
}
}
});
lastCheckedLines.current = lineTexts;
}, [variableTypeName, getLineError, asyncValidateLine, setMessageForLine, setMessagesContainer]);
const handlePaster = (e) => {
e.persist();
setIsPasted(true);
setClipboardData(e.clipboardData.getData("text"));
};
const handleChange = (e) => {
e.persist();
var { keyCode } = e;
var { value } = e.target;
if (keyCode === 13) {
setVariableTypeName(`${value}\n`);
return;
} else if (pasted === true && keyCode === 13) {
setVariableTypeName(`${variableTypeName.concat(clipboardData)}\n`);
setIsPasted(false);
return;
} else if (pasted === true && keyCode !== 13) {
setVariableTypeName(`${variableTypeName.concat(clipboardData)}`);
setIsPasted(false);
return;
} else {
setVariableTypeName(`${value}`);
return;
}
};
return (
<div>
<textarea
placeholder="Variable Types"
maxLength={100}
value={variableTypeName}
onPaste={(e) => handlePaster(e)}
onChange={(e) => handleChange(e)}
/>
{messagesContainer.map((message, i, arr) => {
console.log("message ", message);
return message ? (
<p key={i} className="Messages">{`Error on line ${
i + 1
}: ${message}`}</p>
) : null;
})}
</div>
);
}
export default function App() {
return (
<div className="App">
<TextArea onSave={console.log} />
</div>
);
}
简单版——全部同步,无ref
比较
import "./styles.css";
import React, { useEffect, useState, useCallback } from "react";
export function TextArea({ onSave }) {
const [variableTypeName, setVariableTypeName] = useState("");
const [clipboardData, setClipboardData] = useState("");
const [pasted, setIsPasted] = useState(false);
const [messagesContainer, setMessagesContainer] = useState([]);
const getLineError = useCallback(
(text, index, all) => {
// if too many lines
if (index >= 50) {
return "Only 50 Variable Types allowed.";
}
if (text.length < 3) {
return `Enter at least three characters.`;
}
if (text.length > 20) {
return `Don't exceed more than twenty characters.`;
}
if (!text.match(/^[a-zA-Z0-9_ ]+$/)) {
return `No special characters (but spaces) allowed.`;
}
// check if this line is the same as any of the previous
const duplicateOf = all.slice(0, index).findIndex((v) => v === text);
if (duplicateOf !== -1) {
return `Duplicate of line ${duplicateOf + 1}`;
}
return "";
},
[]
);
useEffect(() => {
const lineTexts = variableTypeName.split("\n");
setMessagesContainer(lineTexts.map(getLineError));
}, [variableTypeName, getLineError, setMessagesContainer]);
const handlePaster = (e) => {
e.persist();
setIsPasted(true);
setClipboardData(e.clipboardData.getData("text"));
};
const handleChange = (e) => {
e.persist();
var { keyCode } = e;
var { value } = e.target;
if (keyCode === 13) {
setVariableTypeName(`${value}\n`);
return;
} else if (pasted === true && keyCode === 13) {
setVariableTypeName(`${variableTypeName.concat(clipboardData)}\n`);
setIsPasted(false);
return;
} else if (pasted === true && keyCode !== 13) {
setVariableTypeName(`${variableTypeName.concat(clipboardData)}`);
setIsPasted(false);
return;
} else {
setVariableTypeName(`${value}`);
return;
}
};
return (
<div>
<textarea
placeholder="Variable Types"
maxLength={100}
value={variableTypeName}
onPaste={(e) => handlePaster(e)}
onChange={(e) => handleChange(e)}
/>
{messagesContainer.map((message, i, arr) => {
console.log("message ", message);
return message ? (
<p key={i} className="Messages">{`Error on line ${
i + 1
}: ${message}`}</p>
) : null;
})}
</div>
);
}
export default function App() {
return (
<div className="App">
<TextArea onSave={console.log} />
</div>
);
}
推荐阅读
- xamarin - 是否可以简化 Xamarin Android 项目的构建配置
- java - Java 字符串长度无法正常工作
- r - 仅对数据框中的可用列求和
- sql-server - AnyConnect 开启时 Azure Ubuntu VM 上的 MSSQL 超时错误
- image - 如何使用 Gabor 滤波器检测对象?
- vert.x - 用于连接 Solace VMR 服务器的 VertX 客户端实现
- apache - Apache 反向代理授权标头
- notepad++ - Notepad ++折叠,使用与打开和关闭相同的字符?
- angular - 发送操作后,ngrx 减速器未触发
- python-3.x - 使用pyautogui解锁屏幕