React - Dynamically set state without hardcoding key field - javascript

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.

Related

Lifting state up in React from all fields in a form as an object

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?

React form - Field is automatically unselected when value changes

I'm a total beginner in React, and am trying to build my first form.
I fetch the questions asked in said form using an API, and once the form submitted, I would like to create an answer object through this same API.
The problem is that everything seems to work just fine (questions render ok, answer object is updated in state), but everytime the value of a field change, I have to re-select the field to keep typing. Basically, it's like I'm auto-clicked away from the field when the value changes.
Here's what happens :
And here's a snippet of (probably) the offending code :
class Form extends React.Component {
constructor(props) {
super(props);
{/* Questions are filled on page load, answers is what I'm working with */}
this.state = { questions: [], answers: [] };
this.handleChange = this.handleChange.bind(this);
}
// This is where the magic is supposed to happen
handleChange(event) {
let key = event.target.id,
value = event.target.value;
{/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */}
this.setState(prevState => {
let copy = Object.assign({}, prevState.answers);
copy[key] = value;
return { answers: copy };
})
}
render() {
const { questions } = this.state;
const TextareaInput = (fieldId) => (
<div>
<textarea name={ fieldId.fieldId } value={this.state.answers[fieldId.fieldId]} id={ fieldId.fieldId } onChange={this.handleChange} ></textarea>
</div>
);
const TextInput = (fieldId) =>(
<div>
<input type='text' name={ fieldId.fieldId } value={this.state.answers[fieldId.fieldId]} id={ fieldId.fieldId } onChange={this.handleChange} />
</div>
);
const allQuestions = (
questions.map((q, key) =>
<div key={q.id} className='question'>
<label htmlFor={ q.field_id } className={ q.required ? 'required' : '' } dangerouslySetInnerHTML={{__html: q.question}}></label>
{q.field_type == 'text' ? <TextInput fieldId={q.field_id}/> : <TextareaInput fieldId={q.field_id}/>}
</div>
)
)
return (
<form>
{ allQuestions }
</form>
)
}
}
export default Form;
(Full component on pastebin)
I think the problem comes from my handleChange function, but I'm not sure what could be causing this. I tried adding some stuffs and moving things around a little without any luck...
You need to call the TextInput and TextareaInput like functions instead of using them like separate components since you defined them within the component.
{q.field_type == 'text'
? TextInput(q.field_id)
: TextareaInput(q.field_id)}
React was unable to keep the reference to them straight and seemingly considered them different elements every render.
Also, as I'm sure you are already aware, you should be careful using dangerouslySetInnerHTML as the name implies, it can be dangerous.
class Form extends React.Component {
constructor(props) {
super(props);
{
/* Questions are filled on page load, answers is what I'm working with */
}
this.state = {
questions: [
{
id: 2,
field_id: 2,
question: 'How are you today?',
field_type: 'text',
},
{
id: 3,
field_id: 3,
question: 'What\'s the answer to life, the universe, and everything??',
field_type: 'textarea',
},
],
answers: [],
};
this.handleChange = this.handleChange.bind(this);
}
// This is where the magic is supposed to happen
handleChange(event) {
let key = event.target.id,
value = event.target.value;
{
/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */
}
this.setState((prevState) => {
let copy = Object.assign({}, prevState.answers);
copy[key] = value;
return { answers: copy };
},()=>{console.log(this.state)});
}
render() {
const { questions } = this.state;
const TextareaInput = (fieldId) => (
<div>
<textarea
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
></textarea>
</div>
);
const TextInput = (fieldId) => (
<div>
<input
type="text"
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
/>
</div>
);
const allQuestions = questions.map((q, key) => (
<div key={q.id} className="question">
<label
htmlFor={q.field_id}
className={q.required ? 'required' : ''}
// As I'm sure you are already aware, this is likely a terrible idea.
dangerouslySetInnerHTML={{ __html: q.question }}
></label>
{q.field_type == 'text'
? TextInput(q.field_id)
: TextareaInput(q.field_id)}
</div>
));
return <form>{allQuestions}</form>;
}
}
ReactDOM.render(<Form/>, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<div id="root" />
In order to avoid using dangerouslySetInnerHTML for the questions, I'd suggest using some sort of markdown renderer. It should be a good enough for most use cases in asking questions.
https://www.npmjs.com/package/react-markdown
https://www.markdownguide.org/
class Form extends React.Component {
constructor(props) {
super(props);
{
/* Questions are filled on page load, answers is what I'm working with */
}
this.state = {
questions: [
{
id: 2,
field_id: 2,
question: 'How are *you* today?',
field_type: 'text',
},
{
id: 3,
field_id: 3,
question: 'What\'s the **answer** to life, the universe, and everything??',
field_type: 'textarea',
},
{id: 4,
field_id: 4,
field_type: 'text',
question:`# This is the big question
#### *ARE YOU READY?*
1. Is this the real life?
1. Or is this just fantasy?
`
}
],
answers: [],
};
this.handleChange = this.handleChange.bind(this);
}
// This is where the magic is supposed to happen
handleChange(event) {
let key = event.target.id,
value = event.target.value;
{
/* Here, my goal is to build an object with questions ids as keys and values of fields as key values. */
}
this.setState((prevState) => {
let copy = Object.assign({}, prevState.answers);
copy[key] = value;
return { answers: copy };
},()=>{console.log(this.state)});
}
render() {
const { questions } = this.state;
const TextareaInput = (fieldId) => (
<div>
<textarea
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
></textarea>
</div>
);
const TextInput = (fieldId) => (
<div>
<input
type="text"
name={fieldId}
value={this.state.answers[fieldId]}
id={fieldId}
onChange={this.handleChange}
/>
</div>
);
const allQuestions = questions.map((q, key) => (
<div key={q.id} className="question">
<label
htmlFor={q.field_id}
className={q.required ? 'required' : ''}
><ReactMarkdown source={q.question}/></label>
{q.field_type == 'text'
? TextInput(q.field_id)
: TextareaInput(q.field_id)}
</div>
));
return <form>{allQuestions}</form>;
}
}
ReactDOM.render(<Form/>, document.querySelector('#root'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-markdown/4.3.1/react-markdown.js" integrity="sha256-4jDgUokdWbazrdnMjWm+TftvBFnOwSNIpvKhgYsInfw=" crossorigin="anonymous"></script>
<div id="root" />
If you need full capabilities of rendering:
https://pragmaticwebsecurity.com/files/cheatsheets/reactxss.pdf, gives an example to use DOMPurify to sanitize the input coming in to prevent cross site scripting and other dangerous behavior rendering outside html can give.
So, for that, you'd purify the string and then pass it into dangerouslySetInnerHTML once it's purified.
I think the questions get lost from your state due to the setState call. AFAIK if setState is given an updater function, it must return the whole state. Shallow merging is only supported if an object is given.
this.setState(prevState => ({
...prevState,
answers: {
...prevState.answers,
[key]: value;
}
}))
(I hope object spread is supported in your environment, If not, do yourself a favor ;-))

onChange handler for Object.keys().map in React to update object properties

I have a component with an empty metadata object at DOM load, the server sends data to fill the empty metadata object with properties that will be assigned values within the form. I am able to iterate through the meta data and see multiple input fields correctly labeled yet when I got to input something it either doesn't change anything and the console logs the single keystroke or it returns TypeError: Cannot read property 'handleChange' of undefined. The title field handles the change just fine.
My code:
class Item extends React.Component{
constructor(props) {
super(props);
this.state = {
title: '',
metadata: {}
}
}
componentDidMount() {
... //retrieve metadata from server
this.setState({
metadata: metadata
});
console.log(metadata); //{meta1: "", meta2: "", meta3: "", meta4: "", meta5: "", …}
}
handleChange = (field) => {
return (value) => this.setState({ [field]: value });
}
render() {
const {
title,
metafield
} = this.state;
}
return(
//code to start form
<TextField value={title} onChange={this.handleChange(title)} label="Title" type=text />
{Object.keys(metadata).map(function(key) {
return (
<TextField key={key} value={metadata[key]} onChange={this.handleChange({key})} label={key} type=text />
)
})}
//code to end form
)
}
I'm sure it's because the handleChange isn't equipped to handle changes on object properties but I'm not sure how to access that layer. I've tried binding a handleMetadataChange function on the constructor and use e.target to assign the values but the failing behavior persists.
There are a couple of bugs:
handleChange sets state like this: this.setState({ [field]: value}); but the values are in state.metadata not in state.
In render
you get metafield from state but initially you set metadata
and in handleChange you don't use any of it.
You always re create onChange for TextField even if nothing has changed, this causes needless DOM re renders.
Here is a working example:
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
metadata: {},
};
}
componentDidMount() {
Promise.resolve().then(() =>
this.setState({
metadata: { x: 'x', y: 'y' },
})
);
}
handleChange = (field, value) =>
//you forgot you are setting metadata of state
this.setState({
...this.state,
metadata: { ...this.state.metadata, [field]: value },
});
render() {
const {
metadata, //you used metaField here but it's metadata
} = this.state;
return (
<div>
{Object.keys(metadata).map(key => (
<TextField
key={key}
value={metadata[key]}
onChange={this.handleChange} //always pass the same handler function
changeKey={key} //added for optimization
label={key}
/>
))}
</div>
);
}
}
//make textfield a pure component as it only receives props
// You could call this TextFieldContainer and not change TextField at all
const TextField = React.memo(function TextField({
value,
onChange,
changeKey,
label,
}) {
const rendered = React.useRef(0);
rendered.current++;
return (
<div>
times rendered: {rendered.current}
<label>
{label}
<input
type="text"
value={value}
onChange={e =>
onChange(changeKey, e.target.value)
}
/>
</label>
</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>
Insert this at the end of your constructor: this.handleChange = this.handleChange .bind(this);
You have to be careful about the meaning of this in JSX callbacks. In JavaScript, class methods are not bound by default. If you forget to bind this.handleClick and pass it to onClick, this will be undefined when the function is actually called.
This is not React-specific behavior; it is a part of how functions work in JavaScript. Generally, if you refer to a method without () after it, such as onClick={this.handleClick}, you should bind that method
Handling Events
class Item extends React.Component{
constructor(props) {
super(props);
this.state = {
title: '',
metadata: {}
}
}
componentDidMount() {
... //retrieve metadata from server
this.setState({
metadata: metadata
});
console.log(metadata); //{meta1: "", meta2: "", meta3: "", meta4: "", meta5: "", …}
}
handleChange = (field,e) => {
let temp = this.state.metdata;
temp[field] = e.target.value;
this.setState({metadata: temp });
}
render() {
const {
title,
metafield
} = this.state;
}
return(
//code to start form
<TextField value={title} onChange={this.handleChange(title)} label="Title" type=text />
{Object.keys(metadata).map(function(key) {
return (
<TextField key={key} value={metadata[key]} onChange={(e)=>this.handleChange(e,key)} label={key} type=text />
)
})}
//code to end form
)
}

React a component is changing an uncontrolled input of type checkbox to be controlled

react gives me a warning: "A component is changing an uncontrolled input of type checkbox to be controlled. Input elements should not switch from uncontrolled to controlled (or vice versa)."
However my checbox is change via the state property. Am I missing something obvious?
import React from 'react';
// Components
import Checkbox from './checkbox';
import HelpBubble from './helpBubble';
export default class CheckboxField extends React.Component {
constructor(props) {
super(props);
this.state = {value: props.value};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
componentWillReceiveProps(nextProps) {
if (nextProps.value !== this.props.value) {
this.setState({value: nextProps.value});
}
}
render() {
const {label, meta = {}, help, disabled, required, onChange} = this.props;
return (
<label className="checkbox-wrap form-field">
<Checkbox
disabled={disabled}
type="checkbox"
onChange={(event) => {
onChange(event, !this.state.value);
}}
checked={this.state.value}/>
{label && (
<div className="checkbox-label">
{label}
{required && <div className="form-field__required"/>}
</div>
)}
{help && <HelpBubble help={help}/>}
{meta.error && meta.touched && (
<div className="input-error">{meta.error}</div>
)}
</label>
);
}}
Parent component:
handleChangeParams(key, value)
}
/>
Handle change params changes the value in model and calls server. Depending on server result, the value can change.
Thanks in advance.
If your state is initialized with props.value being null React will consider your Checkbox component to be uncontrolled.
Try setting your initial state so that value is never null.
this.state = { value: props.value || "" };
If you are using a checkbox react won't like a string either so instead try
this.state = { checkboxValue: props.checkboxValue || false };
Something worth noting about the above code snippet. When you set a state in the constructor from props, it is always best to set the state to a "controlled" value i.e. a tangible value such as an int, float, string, array, map, etc. The error you are getting is the result of props.value being set to null
So, Consider setting your constructor state like this:
this.state = {
value: props.value ? props.value : 'empty'
}
What is happening here is it is checking if props.value has a value, if it does it sets the state to props.value, if props.value is null, it sets the state to the string: `'empty'
Another simple way to do this would be to !! your props.checkboxValue value. That way even if it's undefined, !!props.checkboxValue will resolve to false.
this.state = { checkboxValue: !!props.checkboxValue };
In my case, I was using a prop from my redux store to set whether the checkbox was checked, simply defaulting the property to false worked for me.
e.g.
const MyComponent = ({
somePropFromRedux
}) => {
return <thatThridPartyCheckboxComponent checked={somePropFromRedux} />
}
becomes (only change is adding = false on Line 2)
const MyComponent = ({
somePropFromRedux = false
}) => {
return <thatThridPartyCheckboxComponent checked={somePropFromRedux} />
}
do not use e.target.checked in the inputbox onChange eventHandler method.
Correct way:
const [isChecked, setCheck] = useState(false);
const handler = (e) => {
setCheck(!isChecked);
};
<input
type="checkbox"
checked={isChecked}
onChange={handler}
/>

React - changing an uncontrolled input

I have a simple react component with the form which I believe to have one controlled input:
import React from 'react';
export default class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
return (
<form className="add-support-staff-form">
<input name="name" type="text" value={this.state.name} onChange={this.onFieldChange('name').bind(this)}/>
</form>
)
}
onFieldChange(fieldName) {
return function (event) {
this.setState({[fieldName]: event.target.value});
}
}
}
export default MyForm;
When I run my application I get the following warning:
Warning: MyForm is changing an uncontrolled input of type text to be
controlled. Input elements should not switch from uncontrolled to
controlled (or vice versa). Decide between using a controlled or
uncontrolled input element for the lifetime of the component
I believe my input is controlled since it has a value. I am wondering what am I doing wrong?
I am using React 15.1.0
I believe my input is controlled since it has a value.
For an input to be controlled, its value must correspond to that of a state variable.
That condition is not initially met in your example because this.state.name is not initially set. Therefore, the input is initially uncontrolled. Once the onChange handler is triggered for the first time, this.state.name gets set. At that point, the above condition is satisfied and the input is considered to be controlled. This transition from uncontrolled to controlled produces the error seen above.
By initializing this.state.name in the constructor:
e.g.
this.state = { name: '' };
the input will be controlled from the start, fixing the issue. See React Controlled Components for more examples.
Unrelated to this error, you should only have one default export. Your code above has two.
When you first render your component, this.state.name isn't set, so it evaluates to undefined or null, and you end up passing value={undefined} or value={null}to your input.
When ReactDOM checks to see if a field is controlled, it checks to see if value != null (note that it's !=, not !==), and since undefined == null in JavaScript, it decides that it's uncontrolled.
So, when onFieldChange() is called, this.state.name is set to a string value, your input goes from being uncontrolled to being controlled.
If you do this.state = {name: ''} in your constructor, because '' != null, your input will have a value the whole time, and that message will go away.
Another approach it could be setting the default value inside your input, like this:
<input name="name" type="text" value={this.state.name || ''} onChange={this.onFieldChange('name').bind(this)}/>
I know others have answered this already. But a very important factor here that may help other people experiencing similar issue:
You must have an onChange handler added in your input field (e.g. textField, checkbox, radio, etc). Always handle activity through the onChange handler.
Example:
<input ... onChange={ this.myChangeHandler} ... />
When you are working with checkbox you may need to handle its checked state with !!.
Example:
<input type="checkbox" checked={!!this.state.someValue} onChange={.....} >
Reference: https://github.com/facebook/react/issues/6779#issuecomment-326314716
Simple solution to resolve this problem is to set an empty value by default :
<input name='myInput' value={this.state.myInput || ''} onChange={this.handleChange} />
One potential downside with setting the field value to "" (empty string) in the constructor is if the field is an optional field and is left unedited. Unless you do some massaging before posting your form, the field will be persisted to your data storage as an empty string instead of NULL.
This alternative will avoid empty strings:
constructor(props) {
super(props);
this.state = {
name: null
}
}
...
<input name="name" type="text" value={this.state.name || ''}/>
I had the same problem.
the problem was when i kept the state info blank
const [name, setName] = useState()
I fixed it by adding empty string like this
const [name, setName] = useState('')
In my case, I was missing something really trivial.
<input value={state.myObject.inputValue} />
My state was the following when I was getting the warning:
state = {
myObject: undefined
}
By alternating my state to reference the input of my value, my issue was solved:
state = {
myObject: {
inputValue: ''
}
}
When you use onChange={this.onFieldChange('name').bind(this)} in your input you must declare your state empty string as a value of property field.
incorrect way:
this.state ={
fields: {},
errors: {},
disabled : false
}
correct way:
this.state ={
fields: {
name:'',
email: '',
message: ''
},
errors: {},
disabled : false
}
If the props on your component was passed as a state, put a default value for your input tags
<input type="text" placeholder={object.property} value={object.property ? object.property : ""}>
Set a value to 'name' property in initial state.
this.state={ name:''};
An update for this. For React Hooks use const [name, setName] = useState(" ")
Simply create a fallback to '' if the this.state.name is null.
<input name="name" type="text" value={this.state.name || ''} onChange={this.onFieldChange('name').bind(this)}/>
This also works with the useState variables.
I believe my input is controlled since it has a value.
Now you can do this two ways the best way is to have a state key to each input with 1 onChange handler. If you have checkboxes you will need to write a separate onChange handler.
With a Class component you would want to write it like this πŸ‘‡
import React from 'react';
export default class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = {
myFormFields: {
name: '',
dob: '',
phone: ''
}
}
this.onFormFieldChange = this.onFormFieldChange.bind(this)
}
// Always have your functions before your render to keep state batches in sync.
onFormFieldChange(e) {
// No need to return this function can be void
this.setState({
myFormFields: {
...this.state.myFormFields,
[e.target.name]: e.target.value
}
})
}
render() {
// Beauty of classes we can destruct our state making it easier to place
const { myFormFields } = this.state
return (
<form className="add-support-staff-form">
<input name="name" type="text" value={myFormFields.name} onChange={this.onFormFieldChange}/>
<input name="dob" type="date" value={myFormFields.dob} onChange={this.onFormFieldChange}/>
<input name="phone" type="number" value={myFormFields.phone} onChange={this.onFormFieldChange}/>
</form>
)
}
}
export default MyForm;
Hope that helps for a class but the most performative and what the newest thing the devs are pushing everyone to use is Functional Components. This is what you would want to steer to as class components don't intertwine well with the latest libraries as they all use custom hooks now.
To write as a Functional Component
import React, { useState } from 'react';
const MyForm = (props) => {
// Create form initial state
const [myFormFields, setFormFields] = useState({
name: '',
dob: '',
phone: ''
})
// Always have your functions before your return to keep state batches in sync.
const onFormFieldChange = (e) => {
// No need to return this function can be void
setFormFields({
...myFormFields,
[e.target.name]: e.target.value
})
}
return (
<form className="add-support-staff-form">
<input name="name" type="text" value={myFormFields.name} onChange={onFormFieldChange}/>
<input name="dob" type="date" value={myFormFields.dob} onChange={onFormFieldChange}/>
<input name="phone" type="number" value={myFormFields.phone} onChange={onFormFieldChange}/>
</form>
)
}
export default MyForm;
Hope this helps! 😎
In short, if you are using class component you have to initialize the input using state, like this:
this.state = { the_name_attribute_of_the_input: "initial_value_or_empty_value" };
and you have to do this for all of your inputs you'd like to change their values in code.
In the case of using functional components, you will be using hooks to manage the input value, and you have to put initial value for each input you'd like to manipulate later like this:
const [name, setName] = React.useState({name: 'initialValue'});
If you'd like to have no initial value, you can put an empty string.
In my case component was rerendering and throwing A component is changing an uncontrolled input of type checkbox to be controlled error. It turned out that this behaviour was a result of not keeping true or false for checkbox checked state (sometimes I got undefined). Here what my faulty component looked like:
import * as React from 'react';
import { WrappedFieldProps } from 'redux-form/lib/Field';
type Option = {
value: string;
label: string;
};
type CheckboxGroupProps = {
name: string;
options: Option[];
} & WrappedFieldProps;
const CheckboxGroup: React.FC<CheckboxGroupProps> = (props) => {
const {
name,
input,
options,
} = props;
const [value, setValue] = React.useState<string>();
const [checked, setChecked] = React.useState<{ [name: string]: boolean }>(
() => options.reduce((accu, option) => {
accu[option.value] = false;
return accu;
}, {}),
);
React.useEffect(() => {
input.onChange(value);
if (value) {
setChecked({
[value]: true, // that setChecked argument is wrong, causes error
});
} else {
setChecked(() => options.reduce((accu, option) => {
accu[option.value] = false;
return accu;
}, {}));
}
}, [value]);
return (
<>
{options.map(({ value, label }, index) => {
return (
<LabeledContainer
key={`${value}${index}`}
>
<Checkbox
name={`${name}[${index}]`}
checked={checked[value]}
value={value}
onChange={(event) => {
if (event.target.checked) {
setValue(value);
} else {
setValue(undefined);
}
return true;
}}
/>
{label}
</LabeledContainer>
);
})}
</>
);
};
To fix that problem I changed useEffect to this
React.useEffect(() => {
input.onChange(value);
setChecked(() => options.reduce((accu, option) => {
accu[option.value] = option.value === value;
return accu;
}, {}));
}, [value]);
That made all checkboxes keep their state as true or false without falling into undefined which switches control from React to developer and vice versa.
For people using Formik, you need to add a default value for the specific field name to the form's initialValues.
This generally happens only when you are not controlling the value of the filed when the application started and after some event or some function fired or the state changed, you are now trying to control the value in input field.
This transition of not having control over the input and then having control over it is what causes the issue to happen in the first place.
The best way to avoid this is by declaring some value for the input in the constructor of the component.
So that the input element has value from the start of the application.
Please try this code
import React from "react";
class MyForm extends React.Component {
constructor(props) {
super(props);
this.state = { name: "" };
this.onFieldChange = this.onFieldChange.bind(this);
}
onFieldChange(e) {
this.setState({[e.target.name]: e.target.value});
}
render() {
return (
<form className="add-support-staff-form">
<input name="name" type="text" value={this.state.name} onChange={this.onFieldChange} />
</form>
);
}
}
export default MyForm;
In my case there was spell mistake while setting the state which was causing defined value to be undefined.
This is my default state with value
const [email, setEmail] = useState(true);
my mistake was-
setEmail(res?.data?.notificationSetting.is_email_notificatio);
and the solution is -
setEmail(res?.data?.notificationSetting.is_email_notification);
last char was missing
I had the same issue with type='radio'
<input type='radio' checked={item.radio} ... />
the reason was that item.radio is not always true or false, but rather true or undefined, for example. Make sure it’s always boolean, and the problem will go away.
<input type='radio' checked={!!item.radio} ... />
source
For dynamically setting state properties for form inputs and keeping them controlled you could do something like this:
const inputs = [
{ name: 'email', type: 'email', placeholder: "Enter your email"},
{ name: 'password', type: 'password', placeholder: "Enter your password"},
{ name: 'passwordConfirm', type: 'password', placeholder: "Confirm your password"},
]
class Form extends Component {
constructor(props){
super(props)
this.state = {} // Notice no explicit state is set in the constructor
}
handleChange = (e) => {
const { name, value } = e.target;
this.setState({
[name]: value
}
}
handleSubmit = (e) => {
// do something
}
render() {
<form onSubmit={(e) => handleSubmit(e)}>
{ inputs.length ?
inputs.map(input => {
const { name, placeholder, type } = input;
const value = this.state[name] || ''; // Does it exist? If so use it, if not use an empty string
return <input key={name} type={type} name={name} placeholder={placeholder} value={value} onChange={this.handleChange}/>
}) :
null
}
<button type="submit" onClick={(e) => e.preventDefault }>Submit</button>
</form>
}
}

Categories