A bit of context first, I'm working a questionnaire and decided to start learning React.js. Each step of the questionnaire includes a question and a yes/no radio button group, and also navigation buttons. Here's the jsfiddle. The question component is pretty straight forward, I'm passing the formData and save data function to it in the parent component.
And code for the question component
var Question = React.createClass({
getInitialState: function() {
return {
}
},
createMarkup: function(html) {
return {
__html: html
};
},
render: function() {
var subtitle = this.props.subtitle || '';
var name = this.props.name;
if (this.props.formData[name] === undefined) {
var answer_yes = null;
var answer_no = null;
} else {
answer_yes = this.props.formData[name] == "yes" ? true : null;
answer_no = this.props.formData[name] == "no" ? true : null;
}
console.log(answer_yes);
console.log(answer_no);
return (
<div className="container-question">
<label className="default" dangerouslySetInnerHTML={this.createMarkup(this.props.question)}></label>
<span className="alert subtitle" dangerouslySetInnerHTML={this.createMarkup(subtitle)}></span>
<div className="form-group form-group-radio">
<div className="row">
<div className="col-xs-4">
<input type="radio" id={name + "_yes"} name={name} value="yes" defaultChecked={answer_yes} onChange={this.props.saveData}/><label htmlFor="yes">Yes</label><br></br>
<input type="radio" id={name + "_no"} name={name} value="no" defaultChecked={answer_no} onChange={this.props.saveData}/><label htmlFor="no">No</label>
</div>
</div>
</div>
</div>
);
}
});
The problem is that when you select your answer on the first question and click on next, the radio control on the second question change with it, and same thing happens when you answer second question first. I'm storing the boolean variable for defaultChecked as local variables in the render function and logging them in the console.
Is there any obvious mistake i'm making? any help is appreciated.
Here is an updated jsfiddle that works: https://jsfiddle.net/69z2wepo/8365/
The issue is that you're using what is a called an 'uncontrolled component' in React. See this page for more details: https://facebook.github.io/react/docs/forms.html
Changing to a controlled component for your scenario means using checked instead of defaultChecked. defaultChecked is meant for uncontrolled components where the values of the input controls (in your case radio buttons) are not controlled by the component's props or render method. React is really designed to use controlled components where props/state/render drive exactly what the UI shows.
The first thing you'll notice when changing from using defaultChecked to checked is that you will no longer be able to change the radio state in the UI. The reason this happens is because although the state of your application is being changed in your saveData function, your component is not being re-rendered.
There are a few different approaches to handling this. The updated jsfiddle is re-rendering your application during saveData. Ideally this should be factored differently. Either the saveData should be moved out of the parent component and into a FormDataStore concept (see React Flux architecture, https://facebook.github.io/flux/docs/overview.html) or the parent component should own the formData state and call setState on itself in saveData which will then cause a re-render of that portion of the DOM tree.
Related
I'm going through React's beta docs and would like to better understand how react handles user's interactions with the UI. Specifically with this non-controlled input field.
Note:
the input field is not disabled
new input values should not trigger a render, because it's not changing any state (local variables are ignored)
So, then why is the change not reflected in the UI???
The code is bellow, and an interactive "sandbox" can be found here: https://beta.reactjs.org/learn/state-a-components-memory under challenge #2 at the bottom of the page.
export default function Form() {
let firstName = '';
let lastName = '';
function handleFirstNameChange(e) {
firstName = e.target.value;
}
function handleLastNameChange(e) {
lastName = e.target.value;
}
function handleReset() {
firstName = '';
lastName = '';
}
return (
<form onSubmit={e => e.preventDefault()}>
<input
placeholder="First name"
value={firstName}
onChange={handleFirstNameChange}
/>
<input
placeholder="Last name"
value={lastName}
onChange={handleLastNameChange}
/>
<h1>Hi, {firstName} {lastName}</h1>
<button onClick={handleReset}>Reset</button>
</form>
);
}
To be clear, I'm not asking how to solve this problem; I know how to hook it up and fix it with state. I am interested in what's preventing the input field from updating with a new value.
On initial render it receives the empty string as its value. But then why doesn't it change when a user types?
Here's two non-react input fields, one with a value attribute, and one where the value is set by JS. Both can be modified:
<input type="text" value="editable">
<input type="text" id="input2">
<script>
document.getElementById('input2').value = "also editable"
</script>
So, why isn't the input editable in react?
There are two reasons for a component to render:
It’s the component’s initial render.
The component’s (or one of its ancestors’) state has been updated.
In your example, the second condition is not met and react has no way of knowing your input values changed, it does not re render, and so does not commit changes to the DOM.
Note that react is wrapping event handlers, it does not work exactly like vanilla JavaScript
I am rendering a component including radio inputs. When a radio input selection is made, and the change is updated in state (Zustand if it matters), the component re-renders.
However, the radio selection is persisted on the re-render. Why is this not being cleared on re-render? And/or how to I reset this?
Attached is a simplified case.
class TodoApp extends React.Component {
constructor(props) {
super(props)
this.state = {
count: 0,
}
}
update = () => {
this.setState({
count: 4
})
}
render() {
return (
<div>
<div>
<input id="one" type="radio" name="test"></input>
<label htmlFor="one">one</label>
</div>
<div>
<input id="two" type="radio" name="test"></input>
<label htmlFor="two">two</label>
</div>
<button onClick={this.update}>Update state from 0 to 4</button>
<h2>Count in state: {this.state.count}</h2>
</div>
)
}
}
ReactDOM.render(<TodoApp />, document.querySelector("#app"))
<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="app"></div>
============================================================
Update.
The issue was that I was outputting the radio buttons using map, and assigning the index key attribute.
const radioInputs = allItems.map((item, index) => {
return (
<div key={index}>
<input type="radio" ....../>
<label .....>{item.name}</label>
</div>
)
}
Because the same index was used on subsequent re-rendering, and React uses the key as sort of ID (docs), the next radio input with the same key was selected as the previous selection.
I just needed to use a "globally" unique key, as opposed to the index from map.
its actually Reacts beauty; in React we have virtual dom: abstraction layer on top of real dom. It consists of your React application's elements.
State changes in your application will be applied to the VDOM first. If the new state of the VDOM requires a UI change, the ReactDOM library will efficiently do this by trying to update only what needs to be updated.
For example, if only the attribute of an element changes, React will only update the attribute of the HTML element by calling document.setAttribute
When the VDOM gets updated, React compares it to to a previous snapshot of the VDOM and then only updates what has changed in the real DOM. If nothing changed, the real DOM wouldn't be updated at all. This process of comparing the old VDOM with the new one is called diffing.
thats why your radio buttons dont change when another state changes.
for more on this you can read here.
Currently I am working on a form with the use of react hook forms and I want to save input from a user in the localstorage. At this moment whenever I change the value of the input when loaded from localstorage the input wont change.
When I console.log the event from the onchange method I don't get a log so it didn't trigger the onchange and thats where my problem is. I want that when a user wants to change anything in the form that it should update again.
I have searched on google but I can't find any related problem and so no solution. Since I am a beginner in react I have no clue to solve it myself.
The onchange function is in a functional component same as the input components is only functional.
This is the onchange function that contains the input event.
const onChange = event => {
localStorage.setItem(event.target.id, event.target.value);
};
This is the input compontent
<Input key={index} field={item} formFunction={register({required:true})} checkChange={handleChange} onChange={e => onChange(e)} value={localStorage.getItem(item.label) || ''} errors={errors} selectCheck={handleCountryChange} touched={touched} />
And this is the input compontents code
return (
<div className={props.field.grid}>
<div className={props.field.inputClass}>
<input
type={props.field.type}
name={props.field.name}
className={props.field.class}
id={props.field.label}
data-save="persist"
onBlur={props.checkChange}
style={{ marginBottom: '5px' }}
onChange={props.onChange}
value={props.value}
/>
<label className="left" htmlFor={props.field.label}>{props.field.label}</label>
</div>
</div>
);
Your problem is you are using the local storage to try and update the state of the app so the render function will not get re called and display the new inputted. ( unless you can show more code and you are indeed updating that this.props.value )?
I would suggest looking up local state within component for react, it will make things 10x easier in the future:
React state
Functional component state
You are best creating a local state in your constructor if it is an class component e.g., same can be achieved if it is a functional component just slightly different.
this.state = {
inputVariable: ""
}
then when ever your change this variable(in your onchange function using set state):
setstate({
inputVariable: valueToUpdate
})
your input components value field should be populated with this.state.inputVariable, so as you change the value it will trigger on change and then update the state which will cause a re render of your UI.
if you additionally also to save it to local storage you can do so like you already have.
I have parent React component called Sorter. Inside of Sorter I have child component for range slider:
<div className="child">
<rangeSlider props=... />
</div>
<rangeSlider props={...}/> returns simple input:
render() {
return <input type="text" id={this.props.id} name={this.props.id} value=''/>
}
And then I call module initialization via componentDidMount():
componentDidMount() {
jQuery('#' + this.props.id).ionRangeSlider(config)
}
And everything works fine until I actually use range slider. I have callback in config, which updates parent (in this case it's Sorter's) state. After that range slider just disappears. I tried to reinit it via componentDidUpdate() but it didn't do the trick.
Am I doing something wrong?
I have tried this in codesandbox:
https://codesandbox.io/s/summer-sea-imwpp?fontsize=14
As you can see, it's working fine.
Could you please provide the error message or more details?
Such as :
Is the RangeSlider has to update when the range changed?
How does Sorter change its state with the ionRangeSlider config?
Thanks.
I figured it out. Since I call <RangeSlider props={...} /> render with new props everytime, it triggers React to update DOM and to run re-render.
From the very beginning we have rendered only a single <input> element which has only static attributes that will never change:
<input type="text" id={this.props.id} name={this.props.id} value='' readOnly />
And ionRangeSlider, when called, prepends it's own DOM elements to this input and after this happens we don't need to interact with this input anyhow.
Thus we don't have to re-render it everytime new props arrives. So I just wrapped input in empty <div /> and prevented render from being executed:
shouldComponentUpdate() {
return false
}
render() {
return (
<div>
<input type="text" id={this.props.id} name={this.props.id} value='' readOnly />
</div>
)
}
Yet still I don't think it's a good practice to write dummy methods like this that only returns single static value, but as long as my rendered DOM won't change under no circumstances the situation is under control.
I have child form and input components that are rendered within a parent project component. In the parent component, they are "uncontrolled" as in their value isn't initially set by the parent project's state. Their value is set by the form component state immediately. React keeps giving me this "Warning" (which registers as an error in the console).
Warning: `value` prop on `input` should not be null. Consider using an empty string to clear the component or `undefined` for uncontrolled components.
I understand the issue with controlled vs non controlled components. But how do I get React to see that the child is controlling the input rather than the parent and stop giving me this error?
Other relevant info: this is an external forms module that I created because my apps use a lot of forms and I wanted a standardized way to implement this with just an "npm install ...". This is the sudo code
<Parent project module>
//external forms components
<Form>
<Input/>
<Input/>
<Button/>
</Form>
//end external forms components
</Parent project module>
Here is the render method in the Form component that allows me to control the Input state from withing the Form component. Please let me know if other code snippets or other info would be helpful. Am I going about this incorrectly or is there some way to get rid of these errors? Everything works fine, but it clutters my console and makes it difficult to code.
render() {
const inputs = React.Children.map(this.props.children, child =>
React.cloneElement(child, {
value: this.state.value,
onChange: this.onChange,
error: this.state.userNotify[child.props.name]
})
);
return (
<div id="form-container">
<p className="formTitle">{this.props.formTitle}</p>
<form id={this.props.formID} onSubmit={this.onSubmit} >
{inputs} {/*there must be nested input components passed in*/}
</form>
</div>
)
};
You could try assigning a fallback value to your input components when their state value is not set yet.
const inputs = React.Children.map(this.props.children, child =>
React.cloneElement(child, {
value: this.state.value || '',
onChange: this.onChange,
error: this.state.userNotify[child.props.name]
})
);
This ensures that the value will never be undefined or null thus removing those console warnings.
You must be having value intialized in state
Like in constructor
this.state = {
value: null
}
Or on class level
state = {
value: null
}
So change value: null to value: ""
Thank you both for your suggestions. While they were not solutions in this case, they helped me to think through and identify the problem.
I have a prop on the Input component called prepPopVal. I use it to pre-populate the input with some value. For example; when viewing existing data, you can use my Form/Input component to display that data. It looks like this in the parent project
<Parent project module>
//external forms components
<Form>
<Input prePopVal={this.state.someValue}/>
<Button/>
</Form>
//end external forms components
</Parent project module>
I was using a conditional like this
if(typeof this.props.prePopVal === 'undefined')
var value = this.props.value;
else
var value = this.props.prePopVal;
then placing the "value" variable as the value of the html input
<input className="textinput"
type={type}
id={this.props.name}
name={this.props.name}
value={value}
onChange={this.props.onChange}
autoComplete="off"
/>
What I should have done is eliminated the conditional logic to set "value" and just used the "||" operator within the input to decide which to use, like this:
<input className="textinput"
type={type}
id={this.props.name}
name={this.props.name}
value={this.props.prePopVal || this.props.value}
onChange={this.props.onChange}
autoComplete="off"
/>
This way if "prePopVal" isn't defined it uses "value". This cleared my console error.
Thanks again. I hope this question is useful to someone.