首页 > 解决方案 > 反应 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号线

在此处输入图像描述

任何帮助,将不胜感激!

标签: javascriptreactjsfor-loopreact-hookssetstate

解决方案


您更新单个消息的方法非常复杂。没必要那么难!这是一种通过索引不可变地更新数组的单个项目的方法:

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

代码沙盒链接


推荐阅读