0

Update:

This is a codesandbox example!

I have a textarea component in which I'm preforming validation, line by line:

enter image description here

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. e.g. {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

This is the setMessage function which gets the string:

setMessage is being called inside my validation 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! Line 70

enter image description here

Any help would be appreciated!

15
  • 1
    Hover over that i with the blue background and read what the tooltip says. Consider using console.log(JSON.stringify(variable, null, 2)) if the object supports serialization. Commented Mar 30, 2021 at 21:30
  • Wow thanks! I never new about that! Any ideas how to fix it? Commented Mar 30, 2021 at 21:32
  • 1
    I'm not sure what the problem is; what the data should be, what it is, etc. It would be good to have a minimal reproducible example. Note I don't see the use of setState or hooks in the code right now, so it may be I'm missing something. Commented Mar 30, 2021 at 21:37
  • 2
    I don't need a personal explanation. I'm asking these questions to get you to edit your question and explain in the question so that more people will understand and can help. I'm not necessarily going to answer this question -- I've got paying work to do :). Commented Mar 30, 2021 at 22:05
  • 1
    @HereticMonkey Thanks for suggesting for me to do a minimal reproducible example Not sure why I didn't think of that. Do you think I should re-ask the question? Commented Mar 30, 2021 at 22:56

1 Answer 1

1

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. This prevents updates from interfering with each other if they are done in quick succession and get batched by 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. 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. In the first case we just check if the index is >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.

You are using an async validation library so there is both synchronous and asynchronous validation for each line. The checks that you are doing based on a regex and length could easily be made synchronous to make things simpler.


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

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

Sign up to request clarification or add additional context in comments.

5 Comments

Hi Linda, Thank you so much for your thorough explanation!!!! As you can see from my implementation lot of brute force things going on. However I really wanted to get into useCallbacks! I was reading up on them today, as a matter of fact. I think I got the gist of it. But they're helpful because they prevent creation of function objects between renders?
That's exactly right :) I am using them here because I want the useEffect dependencies to be exhaustive. Meaning that every variable used inside the useEffect should be a dependency. If you want to use a function as a useEffect dependency then you need to use useCallback or else the effect would re-run on every render because the function would be a new instance on every render. If you didn't use useCallback then you would have to omit the functions from the dependency array, which honestly would be fine except that eslint might complain.
Thank you! And I saw the useRef (that is really cool!) I tried something with it but I think I was getting to a point where everything was getting hard to reason about.
I suppose things will become easier with time and experience. Like with everything!
Honestly I would go with the "simple version". The useRef seemed like a good idea but it adds complications and edge cases that need to be addressed. Whereas if you recompute every time it's guaranteed to be correct.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.