简体   繁体   English

反应 setState 钩子不更新循环外

[英]React setState hook not updating OUTSIDE a loop

Update:更新:

This is a codesandbox example!这是一个 代码框示例!

I have a textarea component in which I'm preforming validation, line by line:我有一个 textarea 组件,我在其中逐行执行验证:

在此处输入图像描述

The error messages in the UI are a collection of objects which have an id property based on the row the text is on and a message property which will house the error. UI 中的错误消息是一组对象,这些对象具有基于文本所在行的 id 属性和将容纳错误的 message 属性。 eg {id: 1, message: 'error message', name: 'text from line'}

From what I understand you can't set state in a loop as the results are not guaranteed.a据我了解,您不能在循环中设置 state,因为不能保证结果。

This is the setMessage function which gets the string:这是获取字符串的 setMessage function:

setMessage is being called inside my validation function: setMessage 在我的验证 function 中被调用:

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

This is the whole component:这是整个组件:

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

And this is what is having me pulling what's left of my hair out.这就是让我把剩下的头发拉出来的原因。

In the logs, you can clearly see the objects getting set correctly but then in the UI, the messages are not unique!在日志中,您可以清楚地看到对象设置正确,但在 UI 中,消息不是唯一的! Line 70 70号线

在此处输入图像描述

Any help would be appreciated!任何帮助,将不胜感激!

Your method for updating a single message is extremely complicated.您更新单个消息的方法非常复杂。 It doesn't need to be that hard: Here's a way to immutably update a single item of the array by index:它不需要那么难:这是一种通过索引不可变地更新数组的单个项目的方法:

const setMessageForLine = (message, lineNumber) => {
  setMessagesContainer((existing) => [
    ...existing.slice(0, lineNumber),
    message,
    ...existing.slice(lineNumber + 1)
  ]);
}

We use callback notation to get the current value of messagesContainer as existing .我们使用回调符号来获取messagesContainer的当前值作为existing This prevents updates from interfering with each other if they are done in quick succession and get batched by React.如果更新快速连续完成并由 React 批处理,这可以防止更新相互干扰。

We can reduce the amount of updates that we need to do to the messagesContainer by saving a ref of the last set of lines that we validated.我们可以通过保存我们验证的最后一组行的ref来减少需要对messagesContainer进行的更新量。 If the text is the same as before then we don't need to validate it again.如果文本与以前相同,则我们不需要再次验证它。 But there are some bugs with my implementation of this.但是我的实现存在一些错误。

I think it makes the most sense to have every error assigned to a specific line.我认为将每个错误分配给特定行是最有意义的。 So I am changing the "more than 50 lines" and "duplicate" errors to apply to the line that they occur on.所以我正在更改“超过 50 行”和“重复”错误以应用于它们发生的行。 In the first case we just check if the index is >50 .在第一种情况下,我们只检查索引是否>50 For the duplicates, we compare a text against all previous elements.对于重复项,我们将文本与所有先前的元素进行比较。 This means that the first entry of a duplicate pair won't be an error even if it is duplicated later on.这意味着重复对的第一个条目即使稍后被复制也不会出错。 Now that I am thinking about this, that could pose some problems with the ref if someone were to edit an existing item at a higher line such that it becomes a duplicate of a lower line, since the lower line won't be re-evaluated.现在我正在考虑这个问题,如果有人要在较高行编辑现有项目使其成为较低行的副本,这可能会给ref带来一些问题,因为较低行不会被重新评估.

You are using an async validation library so there is both synchronous and asynchronous validation for each line.您正在使用async验证库,因此每行都有同步和异步验证。 The checks that you are doing based on a regex and length could easily be made synchronous to make things simpler.您基于regex和长度所做的检查可以很容易地同步以使事情变得更简单。


With current async validation使用当前的异步验证

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

Simple version -- all synchronous and no ref comparison简单版本——所有同步且无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>
  );
}

Code Sandbox Link 代码沙盒链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM