React: Infinite function call loop after Slider onChangeEnd - javascript

When I change the value of my slider, it should call a function onChangeEnd. However, when this occurs, the calculateEvent function is called in an infinite loop. This continues forever, even as I keep moving my slider around afterwards. Why does this happen?
var yearStart = 0;
var yearEnd = 50;
var years : number[] = [];
while(yearStart < yearEnd+1){
years.push(yearStart++);
}
const xAxis : number[] = years;
const Savings = () => {
const [initialSavings, setInitialSavings] = React.useState<string>("5000");
const [monthlyDepo, setMonthlyDepo] = React.useState<string>("100");
const [interestRate, setInterestRate] = React.useState<number>(2);
const [yAxis, setYAxis] = React.useState<number[]>([]);
const handleChange = useCallback((newValue) => {
setInterestRate(newValue);
}, []);
const calculateEvent = useCallback((event : string, slider : number, option : number) => {
switch(option) {
case 1:
setInitialSavings(event);
break;
case 2:
setMonthlyDepo(event);
break;
case 3:
setInterestRate(slider);
}
console.log("Calculate event called with slider value: ", interestRate);
getProjection(initialSavings, monthlyDepo, interestRate).then((m) => {
console.log(m);
setYAxis(m);
})
}, [initialSavings, monthlyDepo, interestRate]);
return(
<DefaultLayout>
<Container pt={6}>
<VStack spacing={4}>
<Heading as="h1">Interest Rate Calculator</Heading>
<Heading as="h4">{initialSavings}, {monthlyDepo}, {interestRate}</Heading>
<Input
label="Initial Savings amount"
name="Initial Savings"
onInput={e => calculateEvent(e.currentTarget.value, 0, 1)}
placeholder="5000"
/>
<Input
label="Monthly Deposit"
name="Monthly Deposit"
onInput={e => calculateEvent(e.currentTarget.value, 0, 2)}
placeholder="100"
/>
<Slider
label="Interest Rate"
name="Interest Rate"
value={interestRate}
min={1}
max={15}
step={1}
onChange={handleChange}
onChangeEnd={e => calculateEvent("", e, 3)}
focusThumbOnChange={false}
role="group"
onFocus={(e) => {
e.preventDefault();
}}
/>
<LineChart
title="Savings Over time"
xAxisData={xAxis}
yAxisData={yAxis}
xLabel="Years"
yLabel="Amount"
/>
</VStack>
</Container>
</DefaultLayout>
)
}

Okay, heres what I propose.
Let's break that calculateEvent function up into its separate components.
Then, let's create a useEffect callback that will re-calculate the projection, whenever our inputs change. I'd prefer to do this on render, but we are stuck with an effect because getProjection returns a promise.
Now, I understand why you want to use the onChangeEnd event. getProjection() is expensive, and so you only want to call it, when the user has finished choosing a value.
So let's break it up into two states. One state to manage the slider value. And then a second state to hold the actual interestRate value. We will wait to update the interestRate value until onChangeEnd is called.
Also, let's make sure to control those inputs by putting values on them, and using onChange instead of onInput.
var yearStart = 0;
var yearEnd = 50;
var years: number[] = [];
while (yearStart < yearEnd + 1) {
years.push(yearStart++);
}
const xAxis: number[] = years;
const Savings = () => {
const [initialSavings, setInitialSavings] = React.useState<string>('5000');
const [monthlyDepo, setMonthlyDepo] = React.useState<string>('100');
const [interestRate, setInterestRate] = React.useState<number>(2);
const [interestRateSlider, setInterestRateSlider] = React.useState<number>(2);
const [yAxis, setYAxis] = React.useState<number[]>([]);
const handleSavingsChange = (e) => {
setInitialSavings(e.target.value);
}
const handleMonthlyDepoChange = (e) => {
setMonthlyDepo(e.target.value);
}
const handleInterestRateSliderChange = (value) => {
setInterestRateSlider(value);
};
const handleInterestRateSliderChangeEnd = (value) => {
setInterestRate(value)
}
useEffect(() => {
getProjection(initialSavings, monthlyDepo, interestRate).then((m) => {
setYAxis(m);
});
},[initialSavings, monthlyDepo, interestRate])
return (
<DefaultLayout>
<Container pt={6}>
<VStack spacing={4}>
<Heading as="h1">Interest Rate Calculator</Heading>
<Heading as="h4">
{initialSavings}, {monthlyDepo}, {interestRate}
</Heading>
<Input
label="Initial Savings amount"
name="Initial Savings"
value={initialSavings}
onChange={handleSavingsChange}
placeholder="5000"
/>
<Input
label="Monthly Deposit"
name="Monthly Deposit"
value={monthlyDepo}
onChange={handleMonthlyDepoChange}
placeholder="100"
/>
<Slider
label="Interest Rate"
name="Interest Rate"
value={interestRate}
min={1}
max={15}
step={1}
onChange={handleInterestRateSliderChange}
onChangeEnd={handleInterestRateSliderChangeEnd}
focusThumbOnChange={false}
role="group"
onFocus={(e) => {
e.preventDefault();
}}
/>
<LineChart
title="Savings Over time"
xAxisData={xAxis}
yAxisData={yAxis}
xLabel="Years"
yLabel="Amount"
/>
</VStack>
</Container>
</DefaultLayout>
);
};

Related

How to get inputs data from multiple text box in for loop in React js and pass it to Api

I am trying to build a quiz application where I want to generate no of Question input fields based on admin inputs.
So suppose the admin enters 10 questions for the quiz.
Then I am rendering the form inside for loop for 10 Questions and their answers respectively.
The problem I am facing is I am not able to get all values from input fields.
Below is my demo code:
import { useState } from "react";
const MyComponent = () => {
const [inputs, setInputs] = useState({});
const handleChange = (e) =>
setInputs((prevState) => ({
...prevState,
[e.target.name]: e.target.value
}));
const finalData = (e) => {
e.preventDefault();
console.log("data", inputs);
};
function buildRows() {
const arr = [];
for (let i = 1; i <= 3; i++) {
arr.push(
<div key={i} id={i}>
<input name="Question" onChange={handleChange} />
<input name="option1" onChange={handleChange} />
<input name="option2" onChange={handleChange} />
<input name="option3" onChange={handleChange} />
<input name="option4" onChange={handleChange} />
</div>
);
}
return arr;
}
return (
<>
{buildRows()}
<button
onClick={(e) => finalData(e)}
variant="contained"
className="button-left"
sx={{ marginRight: 3.5 }}
>
Submit Quiz Questions
</button>
</>
);
};
export default MyComponent;
You could use the id (or any other unique property, a unique name would probably be preferred) you're giving your div and build your object with that as an array index like so:
const handleChange = (e) => {
const parent = e.currentTarget.parentNode;
const id = parent.id;
setInputs((prevState) => ({
...prevState,
[id]: {
...prevState[id],
[e.target.name]: e.target.value
}
}));
};
This produces an object like this:
{
"1":{
"Question":"1",
"option1":"2",
"option2":"3",
"option3":"4",
"option4":"5"
},
"2":{
"Question":"6",
"option1":"7",
"option2":"8",
"option3":"9",
"option4":"11"
},
"3":{
"Question":"22",
"option1":"33",
"option2":"44",
"option3":"55",
"option4":"66"
}
}

How to use map function with react-quill without errors?

I am setting up an admin portal, the admin portal can have an unlimited amount of steps input. Each step contains a title and a section of rich text in the form of an HTML string. At the moment my code works but I get this error when I use the arrows to increment the number of steps or if I input 10 or more steps into my input field.
Uncaught Error: You are passing the delta object from the onChange event back as value. You most probably want editor.getContents()
Looks like a React-Quill specific error but something makes me think the error is a side effect of bad code somewhere on my part.
Here is my code:
export const NewsArticlesPage = () => {
const [numberOfSteps, setNumberOfSteps] = useState(0)
//#ts-ignore
const StepsMap = Array.apply(null, { length: numberOfSteps })
let richText: any = { ...StepsMap }
let title: any = { ...StepsMap }
const updateTileArray = (index: number, titleEventData: string) => {
const titleData = { ...title }
title = { ...titleData, [index]: titleEventData }
console.log(title[index])
}
const updateRichTextArray = (index: number, richTextEventData: string) => {
const richTextData = { ...richText }
richText = { ...richTextData, [index]: richTextEventData }
console.log(richText[index])
}
const updateNewsArticleJson = () => {
console.log(richText)
console.log(title)
}
return (
<NewsArticlesWrapper>
<TextField
type="number"
label="Number of steps"
value={numberOfSteps}
InputProps={{ inputProps: { min: 0 } }}
onChange={(e) => setNumberOfSteps(Number(e.target.value))}
/>
{StepsMap.map((n, index) => (
<>
<Typography key={'heading' + index}>Step: {index + 1}</Typography>
<TextField
key={'title' + index}
type="text"
label="Title"
value={title[index]}
onChange={(titleEventData) =>
updateTileArray(index, titleEventData.target.value)
}
/>
<ReactQuill
key={'quil' + index}
theme="snow"
value={richText[index]}
modules={modules}
onChange={(richTextEventData) =>
updateRichTextArray(index, richTextEventData)
}
/>
</>
))}
<Button
variant="contained"
colour="primary"
size="medium"
onClick={updateNewsArticleJson}
>
Submit Article
</Button>
</NewsArticlesWrapper>
)
}
I understand the use of type any is bad but my priority is to get this working then I can add the correct types afterward.

How to display a set number of jsx elements depending on number placed in an input field. React

I have an input field that takes in a number.(between 1 and 30) I want to display an array of items depending on what number is placed in that text field. how can this been done with React hooks. I have something basic for a start like this, but this might not even be the best way to start this.
export default function App() {
const [state, setState] = React.useState({ value: "" });
const [myArray, updateMyArray] = React.useState([]);
const onSubmit = () => {
updateMyArray((arr) => [...arr, `${state.value}`]);
};
const handleChange = (event) => {
let { value, min, max } = event.target;
value = Math.max(Number(min), Math.min(Number(max), Number(value)));
setState({ value });
};
return (
<>
<input
type="number"
onChange={handleChange}
value={state.value}
min={""}
max={100}
/>
<button onClick={onSubmit}>Confirm</button>
{state.value && (
<>
<div>
{myArray?.map((e) => (
<div>{e}</div>
))}
</div>
</>
)}
</>
);
}
You can do it like this
updateMyArray(new Array(state.value).fill(""));
This will create a new array with the length of state.value and asign it to myArray
Maybe this example will be helpful for you.
function App() {
const [amount, setAmount] = useState(0);
const [submittedAmount, setSubmittedAmount] = useState(0);
// optionally
const onSubmit = () => {
setSubmittedAmount(amount);
};
const handleChange = (event) => {
let { value, min, max } = event.target;
value = Math.max(Number(min), Math.min(Number(max), Number(value)));
setAmount(value);
};
return (
<>
<input
type="number"
onChange={handleChange}
value={amount}
min={0}
max={100}/>
<button onClick={onSubmit}>Confirm</button>
{ /* you can use amount instead of submitted amount if you want */
{submittedAmount > 0 && Array.from({ length: submittedAmount }, (_, index) => <div key={index}>{index}</div> )}
</>
);
}
In my opinion if you can skip submitting and use only amount state. Thanks to this your UI will change automatically after input value change without submitting.
If you know the value of value, you can loop till that number, before the render, like:
const items = [];
for (let i; i < state.value; i++) {
items.push(<div>{i}</div>);
}
return (
<div>
{items}
</div>
)

Send values from inputs in react (change querySelector(id) to react)

I am trying to rewrite a small app from vanilla js to react, and in one element I encountered a problem with passing on values in the inputs. What this element does, is after selecting a number it generates that many inputs to fill, and after filling send its id and value further (value can also be empty)
In Vanilla Js I did it with id and querySelector, but in React I have a trouble to change it correct
React code:
import React, { useState, useEffect } from "react";
import "./style.css";
import Values from "./Values";
export default function App() {
const [numberValue, setNumberValue] = useState("");
const [inputValues, setInputValues] = useState([]);
const [sendValues, setSendValues] = useState(false);
const [inputs, setInputs] = useState([]);
let numbers = [4, 6, 8];
//reset teamsName on change teamsValue
useEffect(() => {
for (let i = 1; i <= numberValue; i++) {
setInputValues(prev => [
...prev,
{
id: i,
value: ""
}
]);
}
}, [numberValue]);
const showButtons = numbers.map((number, i) => (
<button
className={`${numberValue === number ? "button active" : "button"}`}
onClick={() => {
setNumberValue(number);
setInputValues([]);
setInputs([]);
showInputs();
}}
>
{number}
</button>
));
//let inputs = [];
const showInputs = () => {
for (let i = 1; i <= numberValue; i++) {
setInputs(prev => [
...prev,
<input
type="text"
className="input"
placeholder={`Input ${i}`}
//value={inputValues.find(input => input.id === i && input.value)}
onChange={e =>
inputValues.filter(
input =>
input.id === i &&
setInputValues([
...inputValues,
{ id: i, value: e.target.value }
])
)
}
/>
]);
}
};
return (
<>
<div className="button-group">{showButtons}</div>
{numberValue && (
<>
<h3 className="title">Your inputs</h3>
<div className="input-group">{inputs}</div>
</>
)}
<button onClick={() => setSendValues(true)}>SEND</button>
{sendValues && <Values inputValues={inputValues} />}
</>
);
}
JS:
const buttonGroup = document.querySelector(".button-group");
const inputGroup = document.querySelector(".input-group");
const inputValues = document.querySelector(".input-values");
let n;
const showInputs = number => {
n = number;
inputGroup.innerHTML = ''
for (let i = 1; i <= number; i++) {
inputGroup.innerHTML += `
<input type="text" name="name" id="input-${i}" class="input" placeholder="team name"> <br>
`;
}
};
let values = []
const showValues = () => {
//clear
inputValues.innerHTML = '';
values = [];
//show new
for (let i = 1; i <= n; i++) {
const input_val = document.querySelector(`#input-${i}`).value;
values.push({
id: i,
value: input_val
});
}
for(let i = 0; i<=n; i++){
inputValues.innerHTML += `
<p>id: ${values[i].id} value:${values[i].value}
</p>
`
}
};
Links to code:
React -> https://stackblitz.com/edit/react-uw9dzc?file=src/App.js
JS -> https://codepen.io/Arex/pen/qBqLVBq?editors=1111
I took the liberty to simplify your code a bit. Basically I assigned value as it's own variable const value = e.target.value; as it is a synthetic event and tends to get lost if you pass it further down, so this preserves the value. Also, I changed inputValues to an object to make it easier to update:
// App.js
export default function App() {
const [numberValue, setNumberValue] = useState("");
const [inputValues, setInputValues] = useState({});
const [sendValues, setSendValues] = useState(false);
let numbers = [4, 6, 8];
const showButtons = numbers.map((number, i) => (
<button
className={`${numberValue === number ? "button active" : "button"}`}
onClick={async () => {
await setNumberValue(number);
await setInputValues({});
}}
>
{number}
</button>
));
return (
<>
<div className="button-group">{showButtons}</div>
{numberValue && (
<>
<h3 className="title">Your inputs</h3>
<div className="input-group">
{[...new Array(numberValue)].map((_value, id) => (
<input
type="text"
className="input"
placeholder={`Input ${id}`}
onChange={e => {
const value = e.target.value;
setInputValues(prev => {
prev[id] = value;
return prev;
});
}}
/>
))}
</div>
</>
)}
<button onClick={() => setSendValues(true)}>SEND</button>
{sendValues && <Values inputValues={inputValues} />}
</>
);
}
// Values.js
const Values = ({ inputValues }) => {
const showValues = Object.keys(inputValues).map(input => (
<div>
{input} : {inputValues[input]}
</div>
));
return <div>{showValues}</div>;
};
export default Values;
There are multiple issues in the shared code as follows:
It is not recommended to store components in state, in the shared code you are storing <input/> component in state. For more details check this
Unnecessary states are being used, always try to keep a minimum number of states as more number of states as more states are needed to be managed, making things unnecesarily complicated.
using previous state syntax to generate new state where it is not needed.
I am adding a working code with minimum changes for you reference.
App.js
import React, { useState, useEffect } from "react";
import "./style.css";
import Values from "./Values";
export default function App() {
const [numberValue, setNumberValue] = useState('');
const [inputValues, setInputValues] = useState([]);
const [sendValues, setSendValues] = useState(false);
let numbers = [4, 6, 8];
//reset teamsName on change teamsValue
useEffect(() => {
setInputValues(
Array(numberValue).fill("")
);
setSendValues(false)
}, [numberValue]);
const showButtons = numbers.map((number, i) => (
<button
className={`${numberValue === number ? "button active" : "button"}`}
onClick={() => {
setNumberValue(number);
}}
>
{number}
</button>
));
return (
<>
<div className="button-group">{showButtons}</div>
{numberValue && (
<>
<h3 className="title">Your inputs</h3>
<div className="input-group">
{inputValues.map((val, i) => (
<input
key={`input${i}`}
type="text"
className="input"
placeholder={`Input ${i+1}`}
value={val}
onChange={(e) => {let newValues = inputValues.slice(); newValues[i]=e.target.value; setInputValues(newValues)}
}
/>
))}
</div>
</>
)}
<button onClick={() => setSendValues(true)}>SEND</button>
{sendValues && <Values inputValues={inputValues} />}
</>
);
}
Values.js
import React from "react";
const Values = ({ inputValues }) => {
const showValues = inputValues.map((input, i) => (
<div key={'output'+i}>
{i+1} : {input}
</div>
));
return <div>{showValues}</div>;
};
export default Values;
I am also sharing a updated stackblitz code reference you shared for better understanding Updated snippet with fixes for reference.

Every change of the input is stored separately as data (ex : "t", "te", "tes", "test" instead of just having "test")

The fact is I'm using an object to store all the data my form collects. In this object, I have an array, and the only solution I've found to update it is to create a function that adds every input into an array and then sets : this.setState({ 'invites': array });
But, the problem is that every change is added to the array and stored.
How can I fix this ?
<input
className="form-input"
type="email"
placeholder="nom#exemple.com"
name="invites"
onChange={e => addToArray(e, "invites")}
required
/>
function addToArray(e, name) {
emailList.push(e.target.value);
props.handleChangeArray(name, emailList);
}
handleChangeArray = (name, array) => {
this.setState({ [name]: array });
};
EDIT:
In the StackSnippet below, I have updated your example (OPs example can be found here) showing how you can get values on submit:
const { useState } = React;
const WORD_LIST = ["Foo", "Bar", "Baz", "Rock", "Paper", "Scissors", "Hello", "Goodbye"];
function InviteInput(props) {
const { value, onChange } = props;
const handleChange = e => {
onChange && onChange(e);
};
return (
<li>
<input
value={value}
onChange={handleChange}
className="form-input"
type="email"
placeholder="nom#exemple.com"
name="invites"
required
/>
</li>
);
}
function AddInviteButton(props) {
return (
<button onClick={props.onClick}>
Ajouter une autre personne // (Add another person)
</button>
);
}
function InviteForm({ onSubmit, initialInputCount }) {
const [nbInvites, setNbInvites] = useState(
[...Array(initialInputCount).keys()].map(i=>WORD_LIST[i])
);
const onAddInviteClick = () => {
//let id = nbInvites.length + 1;
setNbInvites([
...nbInvites,
//{
//id,
//***********************************************************
// THIS IS WHERE YOU SET THE DEFAULT VALUE FOR NEW INPUTS
//***********************************************************
/*value:*/ WORD_LIST[Math.floor(Math.random() * WORD_LIST.length)]
//***********************************************************
//}
]);
};
const handleChange = (event, index) => {
let newstate = [...nbInvites];
newstate[index]/*.value*/ = event.target.value;
setNbInvites(newstate);
};
const handleSubmit = event => {
onSubmit(event, nbInvites);
};
return (
<div>
{nbInvites.map((item, index) => {
return (
<InviteInput
key={index}
value={item}
onChange={e => handleChange(e, index)}
/>
);
})}
<AddInviteButton onClick={onAddInviteClick} />
<br />
<button onClick={handleSubmit}>Soumettre // Submit</button>
</div>
);
}
function App() {
const [formVals, setFormVals] = useState();
const doSubmit = (event, formValues) => {
setFormVals(formValues);
};
return (
<div className="page">
<h2 className="box-title">
Qui sont les eleves de cette classe ? // (Who are the students in this
class?)
</h2>
<p>
Vous pourrez toujours en ajouter par la suite // (You can always add
some later)
</p>
<InviteForm onSubmit={doSubmit} initialInputCount={5} />
{formVals ? <pre>{JSON.stringify(formVals, null, 2)}</pre> : ""}
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
ORIGINAL ANSWER:
You'll need to use the value prop and use event.target.value to assign the value of whatever is typed to the input.
Something like this:
function Test() {
const [inputs, setInputs] = React.useState([{
id: 1,
value: "1 initial"
}, {
id: 2,
value: "2 initial"
}]);
const handleChange = (event, index) => {
let newstate = [...inputs];
newstate[index].value = event.target.value;
setInputs(newstate);
}
const addInput = () => {
let id = inputs.length + 1;
setInputs([...inputs, {
id,
value: `${id} initial`
}])
}
return(
<div>
<button onClick={addInput}>Add Input</button>
{inputs.map((item, index) => {
return <div><input type="text" value={inputs[index].value} onChange={e=>handleChange(e,index)} /></div>
})}
</div>
);
}
ReactDOM.render(<Test />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.9.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
Instead of onChange, we can use onKeyUp to identify user typing complete event,
We can use onKeyUp event to identify user input event and debounce is to identify when user completes entering all the keywords, we need to use loadash for this functionality
It's just one line with underscore.js debounce function:
onKeyUp={e => _.debounce(addToArray(e, "invites") , 500)}
This basically says doSomething 500 milliseconds after I stop typing.
For more info: http://underscorejs.org/#debounce
Updated the above line
<input
className="form-input"
type="email"
placeholder="nom#exemple.com"
name="invites"
onKeyUp={e => _.debounce(addToArray(e, "invites") , 500)}
required
/>
function addToArray(e, name) {
emailList.push(e.target.value);
props.handleChangeArray(name, emailList);
}
handleChangeArray = (name, array) => {
this.setState({ [name]: array });
};
Source - Run javascript function when user finishes typing instead of on key up?

Categories

Resources