ChildComponent displays different fragments depending on the index passed in. This works fine, but if I have input element on multiple fragments and I put a value in one it gets automatically copied to the others. Why is this happening and how can I stop it?
const { Fragment } = React;
const fragments = (onChangeHandler) =>
[
<input type="text" id="screen1_input1" onChange={onChangeHandler} />,
<input type="text" id="screen2_input1" onChange={onChangeHandler} />
];
const ChildComponent = ({ index, fragments }) => {
const onChange = e => {
const { target: {id, value} } = e;
console.log(id, value);
const newData = {
...contentData,
[e.target.id]: e.target.value
}
setContentData(newData)
};
return (
<Fragment>
<h2 className="screens">{fragments(onChange)[index]}</h2>
</Fragment>
);
};
const ParentComponent = props => {
return <ChildComponent index={1} fragments={fragments}/>;
};
ReactDOM.render(<ParentComponent />, document.getElementById("react"));
Give them unique keys like so:
const fragments = (onChangeHandler) =>
[
<input key="key1" type="text" placeholder="input 1" id="screen1_input1" onChange={onChangeHandler} />,
<input key="key2" type="text" placeholder="input 2" id="screen2_input1" onChange={onChangeHandler} />
];
Here a Sandbox to demonstrate it: https://codesandbox.io/s/keen-sun-vsk3e?file=/src/App.js:709-710
React uses the key prop to understand the component-to-DOM Element relation, which is then used for the reconciliation process. It is therefore very important that the key always remains unique, otherwise there is a good chance React will mix up the elements and mutate the incorrect one.
Reference: https://stackoverflow.com/a/43892905/1927991
Related
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.
I'm building a controlled form with dynamic fields.
The Parent component get data from a redux store and then set state with the values.
I don't want to make it with too much code lines so I turn the dynamic fields into a component.
States stay in the parent component and I use props to pass the handlechange function.
Parent :
function EditAbout(props) {
const [img, setImg] = useState("");
const [body, setBody] = useState(props.about.body);
const [instagram, setInstagram] = useState(props.about.links.instagram);
const [linkedin, setLinkedIn] = useState(props.about.links.linkedin);
const [press, setPress] = useState(props.about.press)
const handleSubmit = (e) => {
// Submit the change to redux
};
// set states with redux store
useEffect(() => {
setBody(props.about.body);
setInstagram(props.about.links.instagram);
setLinkedIn(props.about.links.linkedin);
setPress(props.about.press);
}, []);
const handleChangeChild = (e, index) => {
e.preventDefault();
let articles = press
const {value, name } = e.target
if (name === "title") {
articles[index].title = value;
} else {
articles[index].link = value;
}
setPress(articles)
console.log(articles[index])
}
return (
<Box>
<h1>CHANGE ABOUT ME</h1>
<Input
label="Image"
name="img"
type="file"
variant="outlined"
margin="normal"
onChange={(e) => setImg(e.target.files)}
/>
<Input
label="body"
value={body}
name="body"
onChange={(e) => setBody(e.target.value)}
variant="outlined"
multiline
rowsMax={12}
margin="normal"
/>
<Input
label="instagram"
value={instagram}
name="instagram"
variant="outlined"
margin="normal"
onChange={(e) => setInstagram(e.target.value)}
/>
<Input
label="Linkedin"
value={linkedin}
name="linkedin"
variant="outlined"
margin="normal"
onChange={(e) => setLinkedIn(e.target.value)}
/>
<Child press={press} onChange={handleChangeChild} />
{props.loading ? (
<CircularProgress color="black" />
) : (
<Button onClick={handleSubmit} variant="contained">
Send
</Button>
)}
</Box>
);
}
Child :
function Child(props) {
const { press, onChange } = props;
const inputsMarkup = () =>
press.map((article, index) => (
<div key={`press${index}`} style={{ display: "flex" }}>
<input
name="title"
value={press[index].title}
onChange={(e) => onChange(e, index)}
/>
<input
name="link"
value={press[index].link}
onChange={(e) => onChange(e, index)}
/>
<button>Delete</button>
</div>
));
return (
<div>
<h1>Press :</h1>
{inputsMarkup()}
</div>
);
}
Everything is fine when I'm typing in the Parent inputs. But when I'm using Child fields state update for one character but come back at its previous state right after.
It also doesn't display the character change. I can only see it in the console.
Thanks you in advance for your help
The problem is that you're mutating the state directly. When you create the articles variable (let articles = press) you don't actually create a copy and articles doesn't actually contain the value. It's only a reference to that value, which points to the object’s location in memory.
So when you update articles[index].title in your handleChangeChild function, you're actually changing the press state too. You might think that's fine, but without calling setPress() React will not be aware of the change. So, although the state value is changed, you won't see it because React won't re-render it.
You need to create a copy of the press array using .map() and create a copy of the updated array element. You can find the updated handleChangeChild() below:
const handleChangeChild = (e, index) => {
e.preventDefault();
const { value, name } = e.target;
setPress(
// .map() returns a new array
press.map((item, i) => {
// if the current item is not the one we need to update, just return it
if (i !== index) {
return item;
}
// create a new object by copying the item
const updatedItem = {
...item,
};
// we can safely update the properties now it won't affect the state
if (name === 'title') {
updatedItem.title = value;
} else {
updatedItem.link = value;
}
return updatedItem;
}),
);
};
I've created a Letter component, composed of Address, LetterText and Signature.
The state is kept at the Letter level, being the parent.
The method to update the state is propagated to the children as props.
This works well for LetterText which is basically a textarea, but I can't quite get it working for the Address, given it's made of many different input tags.
LetterText looks like this:
export default function Letter(props) {
const [letter, setLetter] = useState({
address: {},
text: "",
signature: null
});
function handleChange(event) {
const value = event.target.value;
setLetter({
...letter,
[event.target.name]: value
});
}
return (
<div>
<Address name="address", letterAddress={letter.address} setLetterAddress={handleChange}/>
<LetterText name="text", letterText={letter.text}, setLetterText={handleChange}/>
<LetterSignature />
</div>
);
}
The LetterText component (working) is as follows:
export default function LetterText(props) {
const { name, letterText, setLetterText, ...rest } = props;
return (
<textarea
name={name}
value={letterText}
onChange={setLetterText}
{...rest}
>
</textarea>
);
}
The Address component is the one I'm struggling with. As you can see I thought about wrapping the setLetterAddress function and do some data massaging. However, this leads to the "
letter.address object to have a pair undefined: undefined in it. Here's my (not working attempt):
export default function LetterAddress(props) {
const { name, letterAddress, setLetterAddress, ...rest } = props;
function handleChange(event) {
const { eName, eValue } = event.target;
let address = { ...letterAddress };
address[eName] = eValue;
const e = { target: { name: name, value: address } };
setLetterAddress(e);
}
return (
<form>
<label>
Full name
<input
type="text"
name="fullName"
value={letterAddress.fullName}
onChange={handleChange}
/>
</label>
<label>
Address
<input
type="text"
name="addressLine"
value={letterAddress.addressLine}
onChange={handleChange}
/>
</label>
</form>
);
}
How can I lift the status up from the Address component nicely?
I'm creating a webapp that allows users to create custom input components. This is in the form of MUI Select and TextFields:
const CTextField = (
<TextField id="outlined-basic" variant="outlined" size="small" />
);
const CSelect = function(match) {
match = match.substring(1, match.length - 1);
const matches = match.split("/");
let menus = [];
matches.forEach(single => {
menus.push(<MenuItem value={single}>{single}</MenuItem>);
});
return (
<FormControl>
<Select>{menus}</Select>
</FormControl>
);
};
The issue I'm coming across is, how can I grab all the values placed or selected from the components. They're created by the end user, so there can be any number of them, and I'm trying to keep it in order with plain text.
What I think works is simply grabbing the raw data including html and stripping it away, then grabbing the values. But because I want this to be dynamic, that'd end up being slow the larger the text is. Is there an efficient method of grabbing all the raw text as well as the values of the input components?
Don't worry about how many elements you have - that's why Arrays are there:
const { useState } = React
const App = (props) => {
const [inputFieldsList, setInputFieldsList] = useState([]);
return (
<div>
<h1>Input field list:</h1>
<button
onClick={() => {
const newVal = [...inputFieldsList, '']
setInputFieldsList(newVal)
}}>ADD INPUT FIELD</button>
{inputFieldsList.map((item, i) => (
<div>
<input
type="text"
onChange={(e) => {
const newList = [...inputFieldsList]
newList[i] = e.target.value
setInputFieldsList(newList)
console.log(inputFieldsList)
}}
/>
</div>
))}
</div>
);
}
const rootElement = document.getElementById('app')
ReactDOM.render(<App />, rootElement)
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="app"></div>
You should use useState for storing the values of the input, and onChange function to set your state when a user types in the input.
const Hello = () => {
const[values, setValues] = useState({
input1: '',
input2: ''
});
const changeHandler = () => {
setValues(
...values,
[event.target.name]: event.target.value
);
}
return (
<>
<input type="text" value={values.input1} name="input1" onChange={changeHandler} />
<input type="text" value={values.input2} name="input2" onChange={changeHandler} />
</>
);
}
In ES6, ComputedPropertyName allows us to do things like use a variable as a key, which in turn means we can set state dynamically. However, if you look around at examples of setting state dynamically, they tend to all have one thing in common -- the state key's name is hardcoded. As an example:
class Input extends React.Component {
state = { state1: "" };
handleChange = event => {
const {
target: { name, value }
} = event;
this.setState({
[name]: value
});
};
render() {
return (
<div>
<label>
<input
type="text"
name="state1"
value="new value"
onChange={this.handleChange}
/>
</label>
</div>
);
}
}
This works because we have a state key called "state1", as seen in the line state = { state1: "" };, and we are hardcoding name in the input field to be that state key, as seen in the line name="state1".
I do not like this solution, because it means I now have to keep track of state.state1" in more than one location. If I were to refactorstate.state1to instead bestate.state2, I would have to go findname="state1"1 and update that to read name="state2". Instead of worry about that, I am wondering if there is a way to set state dynamically without hardcoding this state key. That is, I'm looking to change
<input
type="text"
name="state1"
value="new value"
onChange={this.handleChange}
/>
Into something like:
<input
type="text"
name={this.state.state1.keyname}
value="new value"
onChange={this.handleChange}
/>
Obviously the above doesn't work because keyname is undefined, but the intention here is that name can take on the value of "state1" without me having to hardcode it. How can this be achieved?
You can have an array with objects with keys type and name which you can use to set the initial state and render the inputs dynamically. This way you'll only have to change the value once in the array. You can do something like this.
Here is a codesandbox
import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component {
constructor() {
super();
this.arr = [
{ type: "text", name: "state1" },
{ type: "password", name: "state2" }
];
// set the state keys dynamically from this.arr
this.state = this.arr.reduce((agg, item) => {
agg[item.name] = "";
return agg;
}, {});
}
handleChange = event => {
const {
target: { name, value }
} = event;
this.setState(
{
[name]: value
}
);
};
renderInputs = () => {
return this.arr.map((item, i) => (
<div key={i}>
<label>
<input
type={item.type}
name={item.name}
value={this.state[item.name]}
onChange={this.handleChange}
/>
</label>
</div>
));
};
render() {
const inputs = this.renderInputs();
return <div>{inputs}</div>;
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Hope this helps !
There is the new useReducer() that comes with hooks and context. Check this out i think that is the best pattern to solve your issue. https://reactjs.org/docs/hooks-reference.html.