I am trying to add a new fields in the useState hook
// prevstate
let [currentdata, setcurrentdata] = useState({
id:'',
name:''
})
I try to add new fields in the object like that
setcurrentdata(currentdata => ({
...currentdata,
q: quantity,
}))
but it did not add new fields
The code snippet below may be one possible solution to achieve the desired objective.
NOTE: I personally dislike the idea of using useState in conjunction with such objects & prefer multiple simpler useState on each variable instead.
Code Snippet
const {useState} = React;
const MyFunction = () => {
const [badStateObj, setBadStateObj] = useState({
id: '',
name: ''
});
const clickHandler = (propName) => setBadStateObj(prev => ({
...prev,
[propName]: prev[propName] || ''
}));
const updateHandler = (propName, propVal) => setBadStateObj(prev => ({
...prev,
[propName]: propVal
}));
return (
<div>
{Object.entries(badStateObj).map(([k,v]) => (
<div>
Key: {k} Value: {v}
<button
onClick={() => updateHandler(k, prompt(
`Enter new value for ${k}`
))}
>
Update {k}
</button>
</div>
))}
<button
onClick={() => clickHandler(prompt('Enter prop name'))}
>
Add new prop
</button>
</div>
);
};
ReactDOM.render(
<div>
<h4>Demo showing the useState that I personally dislike</h4>
<MyFunction />
</div>,
document.getElementById("react")
);
<div id="react"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>
Explanation
The badStateObj is initialized with only id and name props
When user clicks the Add new prop button, a new prop may be added
Separate buttons exist to update the value of each individual prop
clickHandler adds the new prop entered by user with '' value.
updateHandler accepts propName and propVal as parameters and updates the appropriate props within the badStateObj.
Related
I have a questions form with custom inputs.
const Input = (props) => {
return (
<div>
<label className={classes.label}>{props.label}
<input className={classes.input} {...props}/>
</label>
</div>
);
};
I get question list from server and set them into questions. Then I create a form for answer to these questions.
<form onSubmit = {onAnswersSubmit}>
{questions?.map((item) =>
<Input key={item.id} id={item.id} label={item.question}/>)}
<Button> Submit </Button>
</form>
I'd like to push answers from inputs into array on submit button click, but have no idea how to do that.
You should probably use state for this. One state for the array (I've called it state), and another to capture the answers to the questions (an object).
When an input's onChange listener is fired it calls handleChange. This function takes the name and value from the input (note: this example assumes that you can add a name property to the data you receive from your server), and then updates the answers state.
When the button is clicked the completed answers state (an object) gets added to the main state array.
const { useEffect, useState } = React;
function Example({ data }) {
// Initialise the states
const [ state, setState ] = useState([]);
const [ answers, setAnswers ] = useState({});
// Push the completed answers object into
// the state array, then reset the answers state
function handleClick() {
setState([ ...state, answers ]);
setAnswers({});
}
// Get the name and value from the input
// and update the answers state
function handleChange(e) {
const { name, value } = e.target;
setAnswers({ ...answers, [name]: value });
}
// Log the main state when it changes
useEffect(() => console.log(state), [state]);
return (
<div>
{data.map(obj => {
const { id, name, question } = obj;
return (
<input
key={id}
name={name}
placeholder={question}
onChange={handleChange}
/>
);
})}
<button onClick={handleClick}>Submit</button>
</div>
);
}
const data = [
{ id: 1, name: 'name', question: 'What is your name?' },
{ id: 2, name: 'age', question: 'How old are you?' },
{ id: 3, name: 'location', question: 'Where do you live?' }
];
ReactDOM.render(
<Example data={data} />,
document.getElementById('react')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>
<div id="react"></div>
You can use the html input's onChange prop and pass an onChange function from your parent form to handle that like so:
// This is assuming this is inside a functional component
const [answers, setAnswers] = useState([]);
const onAnswerChange = useCallback((index, answer) => {
setAnswers((oldAnswers) => {
oldAnswers[index] = answer;
return oldAnswers;
});
}, [setAnswers]);
return
<form onSubmit = {onAnswersSubmit}>
{questions?.map((item, index) =>
<Input key={item.id}
id={item.id}
label={item.question}
value={answers[index] ?? ''}
onChange={(e) => onAnswerChange(index, evt.target.value)}/>)}
<Button> Submit </Button>
</form>
Then you can use the stored state in answers as part of your onAnswersSubmit callback.
const [answers, setAnswers] = useState([]);
const handleAnswerChange = (index, answer) => {
let olddata=answers;
olddata[index]=answer;
setAnswers([...olddata]);
}
const Input = (props) => {
return (
<div>
<label className={classes.label}>{props.label}
<input className={classes.input} {...props}/>
</label>
</div>
);
};
<form onSubmit = {onAnswersSubmit}>
{questions?.map((item) =>
<Input key={item.id} id={item.id} label={item.question} onChange={handleAnswerChange}/>)}
<Button> Submit </Button>
</form>
I'm to add/remove the row dynamically on clicking the button. When I add its adding properly like when there are 1,2,3,4 rows and when I click add on 2 row its adding new row as 3. But when I delete the particular row, its always deleting the last row. Here I've passed the index from map, but even then its removing last element only.
https://codesandbox.io/s/add-remove-items-p42xr?file=/src/App.js:0-1099
Here is a working snippet. It uses a ref to keep track of id's to avoid duplicates, and uses element.order as key instead of index. The remove method has been changed to use a callback passed to setState and a filter() call to remove the elements based on passed order property.
const { useState, useRef } = React;
const App = () => {
const [formValues, setFormValues] = useState([
{ order: 1, type: "", name: "", query: "" }
]);
const id_index = useRef(1);
let addFormFields = () => {
setFormValues([
...formValues,
{ order: (id_index.current += 1), type: "", name: "", query: "" }
]);
};
let removeFormFields = (order) => {
setFormValues(prev => prev.filter(element => element.order !== order));
};
return (
<div>
{formValues.length ?
formValues.map((element) => (
<div className="form-inline" key={element.order}>
<label>{element.order}</label>
<input type="text" name="hello" />
<button
className="button add"
type="button"
onClick={() => addFormFields()}
disabled={formValues.length >= 4}
>
Add
</button>
<button
type="button"
className="button remove"
onClick={() => removeFormFields(element.order)}
>
Remove
</button>
</div>
))
: null}
</div>
);
};
ReactDOM.render(
<App />,
document.getElementById("app")
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.production.min.js"></script>
<div id="app"></div>
Two things you have to update,
update the addFormFields method
update the removeFormFields method
https://codesandbox.io/s/add-remove-items-forked-so-sqoqk?file=/src/App.js:378-394
Here is the codesanbox link for your reference.
I have two components. One components renders a "title" input.
The other component renders a "note" input with 2 buttons.
I have the title input values stored in state called "title"
I have the note input value stored in state called "note"
Now i'm trying to get my title and note values in an object like so:
const [completedNote, setCompletedNote] = useState([{ id=1, title: "", note=""}])
//App component
import React, { useState } from "react"
import NoteTitle from "./components/note-title/NoteTitle";
export default function App() {
const [title, setTitle] = useState("");
const [note, setNote] = useState("");
const [completedNote, setCompletedNote] = useState([
{ id: 1, title: "", note: "" },
]);
return (
<NoteTitle
title={title}
setTitle={setTitle}
note={note}
setNote={setNote}
/>
);
}
//Title Component
import React, { useState } from "react";
import Note from "../note/Note";
export default function NoteTitle({ title, setTitle, note, setNote }) {
return (
<>
<div className="note-maker__maincontainer">
<div className="note-maker__sub-container">
<div className="note-maker__input-container" ref={wrapperRef}>
<div className="note-maker__title">
<input
id="input_title"
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Title..."
onClick={() => setIsNoteDisplayed(true)}
/>
</div>
<Note note={note} setNote={setNote} />
</div>
</div>
</div>
</>
);
}
// Note Component
import React from "react";
export default function Note({ note, setNote }) {
return (
<>
<div className="note__container">
<div className="note-maker__note">
<input
id="input_note"
type="text"
value={note}
onChange={(e) => setNote(e.target.value)}
placeholder="Take a note..."
/>
</div>
<div className="note-maker__buttons-container">
<button className="note-maker__submit-button" type="submit">
Submit
</button>
<button className="note-maker__close-button">Close</button>
</div>
</div>
</>
);
}
How would I go about doing this? I have tried this but its causing "error: To many renders"
setCompletedNote((prevState) =>({
title:{
...prevState.title,
[title]: title,
note:{
...prevState.note,
[note]: note
}
}
}))
Thanks in advance!
If you just want to add a new Completed Note then
Note: Use some library like uuid to generate id and don't do it like below :)
// You have to initiate just an empty array
const [completedNote, setCompletedNote] = useState([]);
// Call this function on submit
const addCompletedNote = () => {
// TODO: validate note and title are not empty
// Add new object to state
setCompletedNote((prevState) => [
...prevState,
{ id: Date.now(), note: note, title: title }
]);
// Clean up existing state
setTitle("");
setNote("");
// Note: this above clean-up triggers state updates 2 times which is not that good but works :) .
// TODO: so try to solve it by combining title, note and completedNote to be a single state object
// like this => { title: "", note: "", completedNote: [] }
// This above change requires a lot of extra changes to work (Try figuring them out :))
}
If you want to update title and note of an existing Completed Note, you need id, newTitle, and newNote values. You update the value of the object that matches the input id.
const updateCompletedNote = (id, newTitle, newNote) => {
setCompletedNote((prevState) => prevState.map(n) => {
if (n.id === id) { // match the id here
return {...n, title: newTitle, note: newNote}; // return new object
}
return n; // objects that do not match id are returned as it is
});
}
You can also update just note or just title But you always need id of the object.
Lets say if you want to just update title of the object you need both id and newTitle and return
return {...n, title: newTitle };
instead of
return {...n, title: newTitle, note: newNote};
Your setCompletedNote function is missing the last closing parenthesis )
You should pass in the arrow function enclosed in curly braces to prevent an infinite loop:
setCompletedNote({
(prevState) => ({
title: {
...prevState.title,
[title]: title,
note: {
...prevState.note,
[note]: note
}
}
})
})
I am implementing a form which is generated using a Json. The Json is retrieved from API and then looping over the items I render the input elements. Here is the sample Json :
{
name: {
elementType: 'input',
label: 'Name',
elementConfig: {
type: 'text',
placeholder: 'Enter name'
},
value: '',
validation: {
required: true
},
valid: false,
touched: false
}
}
Here is how I render the form :
render() {
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
const itemPerRow = 4;
const rows = [
...Array(Math.ceil(props.formElementsArray.length / itemPerRow))
];
const formElementRows = rows.map((row, idx) =>
props.formElementsArray.slice(
idx * itemPerRow,
idx * itemPerRow + itemPerRow
)
);
const content = formElementRows.map((row, idx) => (
<div className='row' key={idx}>
{row.map((formElement) => (
<div className='col-md-3' key={formElement.id}>
<Input
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
handleChange={(event) => props.changed(event, formElement.id)}
/>
</div>
))}
</div>
...
}
I am storing the form state in redux and on every input change , I update the state. Now the problem is everytime I update the state, the entire form is re-rendered again... Is there any way to optimise it in such a way that only the form element which got updated is re-rendered ?
Edit :
I have used React.memo in Input.js as :
export default React.memo(input);
My stateful Component is Pure component.
The Parent is class component.
Edit 2 :
Here is how I create formElementArray :
const formElementsArray = [];
for (const key in this.props.deviceConfig.sensorForm) {
formElementsArray.push({
id: key,
config: this.props.deviceConfig.sensorForm[key]
});
You can make content as a separate component like this.
And remove formElementsArray prop from parent component.
export default function Content() {
const formElementRows = useForElementRows();
formElementRows.map((row, idx) => (
<Input
formId={formElement.id}
handleChange={props.changed}
/>
)
}
Inside Input.js
const handleInputChange = useCallback((event) => {
handleChange(event, formId);
}, [formId, handleChange]);
<input handleChange={handleInputChange} />
export default React.memo(Input)
So you can memoize handleChange effectively. And it will allow us to prevent other <Input /> 's unnecessary renders.
By doing this forElementRows change will not cause any rerender for other components.
You could try a container, as TianYu stated; you are passing a new reference as change handler and that causes not only the component to re create jsx but also causes virtual DOM compare to fail and React will re render all inputs.
You can create a container for Input that is a pure component:
const InputContainer = React.memo(function InputContainer({
id,
elementType,
elementConfig,
value,
invalid,
shouldValidate,
touched,
label,
changed,
}) {
//create handler only on mount or when changed or id changes
const handleChange = React.useCallback(
(event) => changed(event, id),
[changed, id]
);
return (
<Input
elementType={elementType}
elementConfig={elementConfig}
value={value}
invalid={invalid}
shouldValidate={shouldValidate}
touched={touched}
label={label}
handleChange={handleChange}
/>
);
});
Render your InputContainer components:
{row.map((formElement) => (
<div className="col-md-3" key={formElement.id}>
<InputContainer
key={formElement.id}
elementType={formElement.config.elementType}
elementConfig={formElement.config.elementConfig}
value={formElement.config.value}
invalid={!formElement.config.valid}
shouldValidate={formElement.config.validation}
touched={formElement.config.touched}
label={formElement.config.label}
//re rendering depends on the parent if it re creates
// changed or not
changed={props.changed}
/>
</div>
))}
You have to follow some steps to stop re-rendering. To do that we have to use useMemo() hook.
First Inside Input.jsx memoize this component like the following.
export default React.memo(Input);
Then inside Content.jsx, memoize the value of elementConfig, shouldValidate, handleChange props. Because values of these props are object type (non-primitive/reference type). That's why every time you are passing these props, they are not equal to the value previously passed to that prop even their value is the same (memory location different).
const elementConfig = useMemo(() => formElement.config.elementConfig, [formElement]);
const shouldValidate = useMemo(() => formElement.config.validation, [formElement]);
const handleChange = useCallback((event) => props.changed(event, formElement.id), [formElement]);
return <..>
<Input
elementConfig={elementConfig }
shouldValidate={elementConfig}
handleChange={handleChange}
/>
<../>
As per my knowledge, this should work. Let me know whether it helps or not. Thanks, brother.
Explanation
Hi, I'm pretty new in 'advanced' React/Redux field. The problem that I have is:
I didn't use actions so the problem and code can be simplified as much as possible.
MyParent component:
const MyParent = () => {
const ref = useRef(0)
const myArray = useSelector(state => state.someReducer.myArray)
const changeOneItem = () => {
dispatch({ type: CHANGE_ONE_ITEM })
}
return (
<div>
{ref.current ++ }
<button onClick={() => changeOneItem()}>Add</button>
{
myArray.map((item) =>
<MyChild
key={item.id}
name={item.text}
name2={item.text2}
></MyChild>
)
}
</div>
Now here is my child component:
const MyChild = ({name, name2}) => {
const ref = useRef(0)
return (
<div>
<hr/>
<p>{ref.current ++}</p>
<p>{name}</p>
<p>{name2}</p>
</div>
)}
And the reducer:
const initialState = {
myArray: [
{
id: "1",
text: "first",
text2: "001"
},
{
id: "2",
text: "second",
text2: "002"
}
]}
case CHANGE_ONE_ITEM:
return {
...state,
myArray: state.myArray.map(t => t.id == "1" ? {...t, text: "new text"} : t)
}
Question
Let's imagine there is ~1,000 objects inside the array. Everytime I change one of the object inside array, parent component rerenders (because of the selector), which also triggers all child components to rerender.
I'm kind of confused when it comes to immutable changes with Redux, when does immutable change helps if this one is not the case?
Every child component has their own key, but still, whole list will get rerender, is there something I'm missing? Is there a way to trigger render on only one child which corresponding object did change?
Example in main project
Subtitle translator. You will have table, each row will have own textarea where you can write your subtitle for specific timestamp (start of subtitle - end of subtitle). After leaving the textarea, changes should be saved, that save causes lag because each "child" component (in this case each row) rerenders.
Thanks!
Good luck :)
You can make MyChild a pure component with React.memo, your reducer already doesn't change all the other elements of the array t.id == "1" ? {...t, text: "new text"} : t and each MyChild item has a unuque key so none should re render when you only chanage one item but you have to use React.memo because functional components will always re render. That is they will re create jsx but React may not render Dom when current generated jsx is the same as last time.
const { memo, useRef, useCallback, useState } = React;
//using React.memo to make MyChild a pure component
const MyChild = memo(function MyChild({
id,
text,
change,
}) {
const ref = useRef(0);
return (
<div>
<p>Rendered: {++ref.current} times</p>
<input
type="text"
value={text}
onChange={(e) => change(id, e.target.value)}
/>
</div>
);
});
const App = () => {
const [data, setData] = useState(() =>
[...new Array(10)].map((_, i) => ({
id: i + 1,
text: `text ${i+1}`,
}))
);
//use callback so change is only created on mount
const change = useCallback(
(id, text) =>
//set data item where id is the id passed to change
setData((data) =>
data.map((d) => (d.id === id ? { ...d, text } : d))
),
[]//deps array is empty so only created on mount
);
return (
<div>
{data.map((item) => (
<MyChild
key={item.id}
id={item.id}
text={item.text}
change={change}
/>
))}
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>