How to manipulate object property on a reusable component in React - javascript

Im using MUIs input field for a lot of input sections of my questionnaire and want to save it to my state variable that is an object which holds all my form values. How can I manipulate my formData object in a reusable MUI component?
Im currently passing formData as a prop to the component but am unsure how to use the setFormData inside the component since it will be a different property each time I use it.
I was thinking each question is a property of the state object formData so itll look like
formData{question1: 'foo', question2: 'bar'}
This is how the form looks at the moment (i dont mind changing the structure if it makes sense to do so
Take a look at InputField on the form (i also attached the component code)
<QuestionLabel>Do you have a "Nickname":</QuestionLabel>
<InputField value={props.formData.nickname}/>
<QuestionLabel>Preferred Language:</QuestionLabel>
<InputField value={props.formData.language}/>
<QuestionLabel>Occupation:</QuestionLabel>
<InputField value={props.formData.occupation}/>
This is how the component looks (im aware i will have to change this)
export default function InputField(props){
return(
<TextField
fullWidth
value={props.value}
variant='standard'
/>
)
}
Disclaimer
First post so sorry if the format isn't perfect or if i attached the code snippets in a inconvenient way, please give me some pointers if thats the case

Since there is no two-way binding in react, the normal way of processing form input with controlled components is to pull the text from the specific dom element every time it changes. We do this using event.target.value to pull the data, but how do we then add it correctly to your state in a reusable component?
For that we would want to add a name tag to the input field, which we would then use when adding the value to our state. As you correctly stated, formData.someName = 'some text' We update our state in a changeHandler function that updates our state every time the input changes.
For example, assume we are passing setFormData, formData and a fieldName into the props:
export default function InputField(props){
const {setFormData, formData, fieldName} = props //destructure the props
const changeHandler = (event) => {
// updates the formData state of formData.name to equal event.target.value
setFormData({...formData, [event.target.name]: event.target.value})
}
return(
<TextField
fullWidth
name={fieldName}
value={formdata.fieldName}
onChange={changeHandler}
variant='standard'
/>
)
}
Using the above component would look something like this:
<InputField setFormData={setFormData} formData={formData} fieldName="question1">

I was able to get it working by passing the property into the component that needed to be changed; here is my handleChange function on the textfield component:
const handleChange = (event) => {
var formData = {...props.formData}
formData[props.property] = event.target.formData
props.onChange(formData)
}

Related

React - How to share Child component states with Parent?

Background
So I have a simple example, a Form component (Parent) and multiple Input components (Children). Each individual Input component will have an useState hook to initialize and manage its value's state.
Issue
As with most forms, I would like to submit all of the data to a backend for processing. However, the issue is that I cannot retrieve the state of value from each Child Input component.
// app.jsx
import Form from "./Form";
export default function App() {
return <Form />;
}
// Form.jsx
import React from "react";
import Input from "./Input";
const handleSubmit = (e) => {
e.preventDefault();
console.log("Wait, how do I retreive values from Children Inputs?");
};
const Form = () => {
console.log("Form render");
return (
<form onSubmit={handleSubmit}>
Sample Form
<Input initial="username" name="user" />
<Input initial="email" name="email" />
<button type="submit">Submit</button>
</form>
);
};
export default Form;
// Input.jsx
import React from "react";
import useInputValue from "./useInputValue";
const Input = ({ name, initial }) => {
const inputState = useInputValue(initial);
console.log(`${name}'s value: ${inputState.value}`);
return <input {...inputState} />;
};
export default Input;
Plausible Solution
Of course, I can lift the Input states up to the Form component, perhaps in an obj name values. However, if I do that, every time I change the Inputs, the Form will re-render, along with all of the Inputs.
To me, that is an undesirable side-effect. As my Form component gets bigger, this will be more costly to re-render all inputs (inside the form) every time one input changes.
Because of that, I would like to stick with my decision of having each individual input manage its own state, that way if one input changes, not all other input will re-render along with the Parent component.
Question
If each of the Child components manages its own state, could the Parent component access the value of that state and do actions (like form submission)?
Update
Many answers and comments mentioned that this is premature optimization and the root of all known evil; which I agree with, but to clarify, I am asking this question with a simple example because I wanted to find a viable solution to my current and more complex project. My web app has a huge form (where all states are lifted to the form level), which is getting re-rendered at every change in its inputs. Which seemed unnecessary, since only one input is changed at a time.
Update #2
Here is a codesandbox example of the issue I am having. There are two forms, one with all states managed by individual Child input components, and the other has all states lifted up in the form level. Try entering some values and check the console for log messages. One can see that with all states lifted to the form level, every change will cause both inputs to be re-rendered.
I think yes, you can share state. Also there are 3 options:
I recommend you to use such library as Formik. It will help you in your case.
You can share state using useState() hook as props.
Use such tools as Redux Toolkit (if we are speaking about memoisation), useContext() and etc.
If the thing you want is getting final values from input, assign ref to each input and access using emailRef.current.value in the submit function.
import { useState, useRef, forwardRef } from 'React';
const Input = forwardRef((props, ref) => {
const [value, setValue] = useState('');
return <input ref={ref} value={value} onChange={(e) => {setValue(e.target.value)}} {...props} />;
});
const App = () => {
const emailRef = useRef(null);
const submit = () => {
const emailString = emailRef.current.value
};
return (
<>
<Input ref={emailRef} />
<button onClick={submit}>Submit</button>
</>
);
};
If the parent needs to know about the childs state, you can
move the state up. This is resolved by passing down a setState function that the child calls (the states data is available in the parent)
use a context https://reactjs.org/docs/context.html
use a state management library e.g. redux
In your simple case I'd go with 1)

React best practice for changing state of parent component from child without rerendering all children?

I have a project where I'm displaying cards that contain attributes of a person in a textfield, and the user can edit the textfield to directly change that person's attribute values. However every time they're editing the textfield it causes a rerender of all cards which slows down the app. Here is an example:
export default Parent() {
const [personList, setPersonList] = useState(/* list of person objects*/);
const modifyPerson(index, property, value) {
const newPersonList = _.cloneDeep(personList);
newPersonList[index][property] = value;
setPersonList(newPersonList);
}
const children = personList.map((person, index) => {
<Person
modifyPerson={modifyPerson}
index=index
/*properties on the person */
/>
});
return <div> {children} </div>
}
export default Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
}
So essentially when the child's text field is updated, it triggers a state change in the parent that stores the new value, then refreshes what the Child looks like. There's no button that the user clicks to "save" the values-- as soon as they edit the textfield it's a permanent change. And also the parent needs to know the new values of every Person because there's some functions that require knowledge of the current state of the person list. The Person component contains images that slow down the rendering if done inefficiently.
Is there a better way to make this design more performant and reduce rerenders? I attempted to use useCallback to preserve the functions but I don't it works in this specific design because the property and values are different-- do I have to create a new "modifyPerson" for each exact attribute?
Use React.memo()
React.Memo will check the props passed to the component and only if the props changes , it will re-render that particular component.
So, if you have multiple Person component which are getting props which are explicit to those Person, and when you change a particular Person component which leads to Parent state getting updated, only that Person component which you had modified will re-render because its props will change(assuming that you pass the changed value to it).
The other components will have the same props and wont change.
export default React.memo(Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
})
As others have already said React.memo() is the way to go here, but this will still re-render because you recreate modifyPerson every time. You can use useCallback to always get the same identity of the function, but with your current implementation you would have to add personList as dependency so that doesn't work either.
There is a trick with setPersonList that it also accepts a function that takes the current state and returns the next state. That way modifyPerson doesn't depend on any outer scope (expect for setPersonList which is guaranteed to always have the same identity by react) and needs to be created only once.
const modifyPerson = useCallback((index, property, value) {
setPersonList(currentPersonList => {
const newPersonList = _.cloneDeep(currentPersonList);
newPersonList[index][property] = value;
return newPersonList;
})
}, []);

React.js Using the same FORM for creating a NEW db object and EDIT various objects

I have a simple input field like this:
<TextField
required
variant="standard"
type="string"
margin="normal"
fullWidth = {false}
size="small"
id="orgName"
placeholder="Organisation Name"
label="Name"
name="orgName"
// defaultValue={orgData ? orgData.orgName : ""}
//inputRef={(input) => this.orgName = input}
value={this.state.orgName || ""}
onChange={this.handleChange("orgName")}
error={this.state.errors["orgName"]}
/>
I want to use the same input field for new and update? For new I just set the state to empty, and save the values. Which works fine. Now I have a select dropdown to edit the previously saved objects.
My problem is with editing, and I am tearing my head out trying to find the any way to do this. All these are the corresponding issues:
If i set the state from props - any edited changes are being reset
If i don't set the state from props, I get all blank fields, which is incorrect.
If I use defaultValue to load the form inputs from props, then its only called once. And it does not reload when I change the object to be edited.
If i just use onChange handler for, the form gets creepy slow, with many inputs on page
If I use refs, I am not able to reset the refs to reload the input when the object to be edit changes.
I have managed to make it work with componentWillReceiveProps, but it is deprecated, and react website is saying its dangerous to use it.
componentWillReceiveProps(nextProps) {
if (nextProps.orgData !== this.props.orgData) {
this.setState({
"orgName": nextProps.orgData.orgName,
"orgPath": nextProps.orgData.orgPath,
"orgAddress": nextProps.orgData.orgAddress,
"orgPhone": nextProps.orgData.orgPhone,
"orgEmail": nextProps.orgData.orgEmail,
})
}
}
So how can I actually create an editable form where the values have to be loaded from props for different instances from db, but they have to controlled by the state. There has to someplace where I have to check saying "hey if the props have changed, reset the state with the new props for edit???
This has been the most frustrating experience using react for me. How are there no examples anywhere on how to build a simple form to create new, and editable object using react and redux. It just seems overly complicated to do such a simple thing, the whole thing just sucks!
There has to someplace where I have to check saying "hey if the props have changed, reset the state with the new props for edit???
Yes you can use React.useEffect hook on the special prop or Array of props, then when that/those prop(s) change the internal hook function will be fire.
e.g. :
const MyComponent = (props) => {
let [myState,setMyState]= React.useState(null);
React.useEffect(()=>{
setMyState(props.prop1);
},[props.prop1]);
return ...
}

Dynamically add attributes in React render input

I have an input tag in render like this:
<input id="time" type="time">
and I need dynamimically add value attribute
How can I do this in React? Thanks in advance
Yes, you can do. e.g. props is an object which contains props or properties that you want to add based on some condition then you can something like
const props = { id: 'time', type: 'time' };
if (condition1) {
props.value = 'some value';
}
if(condition2) {
props.abc = 'some other value';
}
<input {...props} >
You should add an onchange attribute on the input which changes the state of your component. This will look something like this:
<input type="text" value={this.state.value} onChange={this.handleChange} />
The handleChange method should then look something like this:
handleChange = (event) => {
this.setState({value: event.target.value});
}
For more information check out the following article: https://reactjs.org/docs/forms.html
You would need to set the state of React. So...
this.setState({
myValue: newValue
})
Then, inside render:
<input id="time" type="time" value={this.state.myValue}>
This assumes you have your state setup with a constructor. Which is a whole other can of worms.
You shouldn't dynamically add or remove a "value" field. When you create a React input, it must be either "controlled" or "uncontrolled" through its whole lifespan. Changing it will make React yell a warning on the console.
React understads an input is meant to be uncontrolled if value is not present or undefined, so you need to at least set it to "" in order to achieve a controlled empty input.
"Controlled" means react controls its value. Ex: For the value to be changed, you'd need to notify react of the change (through onChange + setState) and then make it change its value;
"Uncontrolled" means react can't control the input's value, and you'd read and change the value through regular DOM ways (ex: input.value).
That being said, in order to dynamically change the presence of any element properties (props), you can use the "object spread" operator.
function render() {
const custom = { value: 2, color: randomColor() }
return <Element {...custom}/>
}
I guess you are rendering the tag within a render() function within a JSX expression (otherwise you would have added it through normal Javascript or JQuery).
Therefore the JSX expression will have something like:
<input id="time" type="time" value={yourValue}>
making sure that yourValue is in scope within the context of execution of your render() in your ReactComponent class or in the props. Alternatively it could be in the state.

Entire form being rerendered in React with Redux state

I have dynamic JSON data and have a custom method to go through the JSON to dynamically render a form on the page. The reason for the JSON schema is to build various forms that is not predefined.
I have hooked up Redux so that the schema and the formValues below gets assigned as the props of this class. So, the form is rendering correctly with the correct label, correct input field types etc. When an onChange event happens on the fields, the app state(under formData) is being updated correctly. But I am noticing that when the formData changes in the app state, the entire form gets re-rendered, instead of just the "specific fields". Is this because I am storing the form values as an object under formData like this? How do I avoid this issue?
formData = {
userName: 'username',
firstName: 'firstName
}
Example schema
const form = {
"fields":[
{
"label":"Username",
"field_type":"text",
"name":"username"
},
{
"label":"First Name",
"field_type":"text",
"name":"firstName"
}
]
}
Redux state
const reducer = combineReducers({
formSchema: FormSchema,
formData: Form
});
//render method
render() {
const { fields } = this.props.form,
forms = fields.map(({label, name, field_type, required }) => {
const value = value; //assume this finds the correct value from the "formData" state.
return (
<div key={name}>
<label>{label}</label>
<input type={field_type}
onChange={this.onChange}
value={value}
name={name} />
</div>
);
})
}
//onchange method (for controlled form inputs, updates the fields in the formData app state)
onChange(event) {
this.props.dispatch(updateFormData({
field: event.target.name,
value: event.target.value
}));
}
From your example I'm not sure, but if you're rendering the whole thing in a single render() method, yes, the component will be rendered again. And that is the problem, THE component. If you are trying to have multiple components, then they should be split up as much as possible. Otherwise if the state changes, it triggers a re-render of the only component there is.
Try breaking it as much as you can.
Hints: (dont know if they apply but maybe)
use ref={}s
implement shouldComponentUpdate()
EDIT: Just thought about this, but are you storing the fields' values in your state? This doesnt feel correct. Be sure to read carefully the React guide about controlled components. (Eg try to render using plain <span>s instead of inputs, and listen to onKeyPress. Would it still work? If not you might be misusing the value attribute)

Categories