How to allow updates to input without updating props in React? - reactjs

I would like to allow a user to enter changes in an input field without propagating them to the parent. I did this by returning out of the onChange function whenever I don't want to propagate. However this seems to undo the character I typed.
Here is a use case. I have a number field. I want to trigger onChange on parent when there is a number entered, but ignore "." and ","s (formatters.staticToFloat removes them).
export default class NumberField extends React.Component {
render () {
var props = this.props;
var format = props.formatter || formatters.number;
return (
<div>
<label>{props.inputLabel}</label>
<input
type="text"
name={props.name}
onChange={this.onChange.bind(this)}
value={!_.isUndefined(props.value) ? format(props.value) : null}
/>
</div>
);
}
onChange (e) {
var numValue = formatters.stringToFloat(e.target.value);
//they added a . or , we don't propagate change
if (this.props.value === numValue) {
return;
}
if (this.props.onChange) {
this.props.onChange({
value: numValue,
valid: validation.isValid(numValue, this.props.validation)
});
}
}
};

So far the best way I've come up with is maintaining a separate formattedValue state that only gets set when I want to override the default formatting. It works, but seems like a super dirty solution.
export default class NumberField extends React.Component {
constructor () {
super();
this.state = {
formattedValue: null
};
}
render () {
var props = this.props;
var state = this.state;
var format = props.formatter || formatters.number;
var inputValue = state.formattedValue || (
!_.isUndefined(props.value) ?
format(props.value) :
null
);
return (
<div>
<label>{props.inputLabel}</label>
<input
type="text"
name={props.name}
onChange={this.onChange.bind(this)}
value={inputValue}
/>
</div>
);
}
onChange (e) {
var numValue = formatters.stringToFloat(e.target.value);
//they added a . or , we don't propagate change
if (this.props.value === numValue) {
this.setState({
formattedValue: e.target.value
});
} else {
this.setState({
formattedValue: null
});
}
if (this.props.onChange) {
this.props.onChange({
value: numValue,
valid: validation.isValid(numValue, this.props.validation)
});
}
}
};

Sounds like a case for using state to store the formatted value inside the component, and use a special variant of setState with a callback.
all user input (including . and ,) is put in state and rendered in input field back to user.
only when the format is correct, the parent onChange() is called.
The parent may actually do a re-render triggered by the onChange() call. Therefore, we need to make sure that the last entered character is updated in state, and only after that the parent's onChange() will be called.
Your component would look like as follows:
export default class NumberField extends React.Component {
constructor (props) {
super(props);
this.state = {
value: !_.isUndefined(props.value) ?
props.formatter ? props.formatter(props.value).toString() : formatters.number(props.value).toString()
: null;
};
}
render () {
var props = this.props;
var state = this.state;
return (
<div>
<label>{props.inputLabel}</label>
<input
type="text"
name={props.name}
onChange={this.onChange.bind(this)}
value={state.value}
/>
</div>
);
}
onChange (e) {
var numValue = formatters.stringToFloat(e.target.value);
// if numValue is different from current state
// then it must be an OK update,
// so we update state AND call parent if function exists
if (numValue.toString() != this.state.value) {
this.setState({
value: numValue
},
this.callParent // here is the magic: we pass a callback, to be called after state update and after re-render
);
} else {
// otherwise we only update state (to display invalid character)
this.setState({
value: numValue
});
}
}
callParent() {
// state is updated and component has re-rendered when this is called
// so we can use state.value to inform parent
if (this.props.onChange) {
this.props.onChange({
value: this.state.value,
valid: validation.isValid(this.state.value, this.props.validation)
});
}
}
};
This may be a bit of overkill: you keep a state (formatted value) that you also communicate to the parent. If your parent ALWAYS passes down the newly forwarded input, then you could make your component a lot simpler:
No state, but simply re-render based on props.
You only really need state if the value presented to the user in the input field can deviate from what you communicate to parent.

Related

Pass state as props from parent to child and then update parent from child onSubmit

I have a parent component that renders a child component and passes it's initial state to the child component. I need a few clarifications- My gut feeling is to handle the event change in the Child component, is this correct? Upon submission, how do I pass the updated props back to parent? My gut is also telling that once the props is passed back to the parent, I can use componentDidUpdate() to set the state to be used elsewhere. If so, how?
class Parent extends React.Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "destination"
};
}
componentDidUpdate(){
// how to update state?
}
render() {
const { arrival, departure } = this.state;
return <Child arrival={arrival} departure={departure} />;
}
}
class Child extends React.Component{
constructor(){
this.handleSubmission = this.handleSubmission.bind(this);
}
handleSubmission(e){
const target = e.target;
const name = target.name;
const value = target.value;
// not sure how to handle props from here
}
render(){
let { arrival, departure } = this.props;
return(
<form onSubmit = {this.handleSubmission} >
<div class="form-group">
<label for="departure">Departure</label>
<input type="" class="form-control" name="departure" aria-describedby="emailHelp" placeholder="Enter Departing Station"/>
</div>
<div class="form-group">
<label for="arrival">Arrival</label>
<input type="password" class="form-control" name="arrival" id="inputArrival" placeholder="Enter Arriving Station"/>
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
)
}
}
export default Child
#hello_there
Please disregard the previous answer, as it requires a lot more than just changing a few props.
I've forked the Sandbox and rewrote it here (so you can follow along).
I've outlined the steps to make the change to propagate to the parent below.
Capture the states of <input />
First step is to capture the state of form fields.
There are two ways to handle form fields
Controlled Components
Uncontrolled Components - discouraged
I am going to use the former (controlled) to capture the form field states by adding a state to Form component.
And you need to set the value={...} of each state and update each state from onChange event (using handleInputChange added below) for each form field.
I've added 👇 where changes were made
import React, { Component } from "react";
class Form extends Component {
// .. 👇
state = {
departure: "",
arrival: ""
};
//... rest removed for brevity
// .. 👇 is used to update each form field state.
handleInputChange = e => {
e.preventDefault();
const { name, value } = e.target;
this.setState({ [name]: value });
};
render() {
const { departure, arrival } = this.state;
return (
<form onSubmit={this.handleSubmission}>
<div className="form-group">
<label> Departure</label>
<input
className="form-control"
name="departure"
placeholder="Enter Departing Station"
// ... 👇 ...👇
value={departure} onChange={this.handleInputChange}
/>
</div>
<div className="form-group">
<label> Arrival</label>
<input
className="form-control"
name="arrival"
id="inputArrival"
placeholder="Enter Arriving Station"
// ... 👇 ...👇
value={arrival} onChange={this.handleInputChange}
/>
</div>
<button type="submit" className="btn btn-primary">
Submit
</button>
</form>
);
}
}
Update App's state change event handler
Now we have states handy, we need to update the App's updateState to accept a whole new state, so we don't make multiple method calls (this.props.updateParentState) and it'd let us pass a new reference so that React would know that the state has been changed in the App component.
class App extends Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "departure"
};
}
// From this 👇
// updateState = (name, value) => {
// this.setState({
// [name]: value
// });
// };
// to this 👇
updateState = newState => this.setState(newState);
componentDidUpdate(prevProps, prevState) {
const { arrival, departure } = this.state;
console.log(`Arrival: ${arrival}, departure: ${departure}`);
}
render() {
const { arrival, departure } = this.state;
return (
<Fragment>
<Form
arrival={arrival}
departure={departure}
// 👇 stays the same
updateParentState={this.updateState}
/>
</Fragment>
);
}
}
Update Child's submission event handler
Now the App.updateState accepts a state object, which can be used to update App.state, let's change Child.handSubmission.
handleSubmission = e => {
e.preventDefault();
// this.props.updateParentState(name, value);
this.props.updateParentState(this.state);
};
You can see that this.props.updateParentState(name, value) has been replaced with this.props.updateParentState(this.state), which would let us update App.state at once.
Now you should be able to see the change in the App.componentDidUpdate.
OLD ANSWER - Disregard this
Changing the state in the child doesn't cause the re-render in the parent (so componentDidUpdate is probably not triggered by change in child component). So what you can do is to pass the event handler down to the child, which the child can notify the Parent component that something has changed.
I've explained the flow of how you can update the parent's states.
First you need to create a handler, with which you can update the Parent's state with.
updateState = (name, value) => this.setState({ [name]: value });
Here, I am using [name], which is a dynamic property. So it should match up with the Parent's state name. In this case either arrival or departure.
Then you need to pass that event handler to the Child (you can name the prop name to whatever (In the code below, I used updateParentState but it can be updateWhatever so long as you pass it the updateState correctly).
<Child arrival={arrival} departure={departure} updateParentState={updateState} />
Here is the complete code that will update the parent state.
Changes are indicated with "👇" emoji below.
class Parent extends React.Component {
constructor() {
super();
this.state = {
arrival: "arrival",
departure: "destination"
};
}
// This is what you pass to the child as a `prop` to call.
updateState = (name, value) => this.setState({ [name]: value });
render() {
const { arrival, departure } = this.state;
// pass the event handler to the child component ... 👇 ...
return <Child arrival={arrival} departure={departure} updateParentState={updateState} />;
}
}
class Child extends React.Component{
constructor(){
this.handleSubmission = this.handleSubmission.bind(this);
}
handleSubmission(e){
const target = e.target;
const name = target.name;
const value = target.value;
// 👇 Update the "Parent"'s state.
// `name` & `value` will be the supplied to `Parent.updateState`
this.props.updateParentState(name, value)
}
render(){
// ... removed for brevity
}
}
If the statement management gets too hard to manage with prop-drilling, then you can reach out for Context API (after familiarizing with it, you can check out How to use React Context effectively, which uses Hooks).
Or you can use Redux.
Just pass to your children a reference on how to update the state:
class Parent extends React.Component{
state = {departure : ''}
setDeparture = departure => this.setState({ departure })
render(){ <Child setDeparture={this.setDeparture} /> }
}
const Child = ({setDeparture}) => <button onClick={() => setDeparture('foo')}>Click</button>

Trouble updating radio button state value in parent component from child element

I'm currently working a a multipage checklist app to make a common checklist procedure more efficient.
my parent component called MainForm has all of the states for my app. In my first child element, I had to fill some text inputs. The states are updating and saving as planned. My second page (or other child element) was the portion where my checklist would begin. The issue is my app is rending, but the radiobutton value isn't being sent to my state. I'm also having an issue where I can select the 'yes' radio button and then the 'no' radio button, but I can't go from 'no' to 'yes'. radioGroup21 is the radio group that's giving me problem. All other states are working.
I'm getting an error in my console that says:
"Checkbox contains an input of type radio with both value and defaultValue props. Input elements must be either controlled or uncontrolled (specify either the value prop, or the defaultValue prop, but not both). Decide between using a controlled or uncontrolled input element and remove one of these props.
I've tried removing the value tag and the defaultValue line in my Radio elements, but no luck. I've tried creating constructor(props) in my parent element but I still kept having issues."
So far I've tried removing the defaultValue in my radio button and after I tried removing the value line. Unfortunately I this did not help.
I also read about controlled and uncontrolled inputs. I've tried changing my parent components state to put them in a constructor(props) bracket. But no luck.
I also tried to not use the handleChange function and use the setState function with values of {radioButton21 === 'yes'} but that didn't work.
//Parent Component
Class MainForm extends Component {
state = {
step: 1,
projectNumber: '',
projectName: '',
numberOfSystems: '',
buildSheet: '',
controlPhilosophy: '',
projectLayoutDrawing: '',
projSoftwareValidation: '',
CppDrawing: '',
radioGroup21: '',
}
nextStep = () => {
const { step } = this.state
this.setState({
step : step + 1
})
}
prevStep = () => {
const { step } = this.state
this.setState({
step : step - 1
})
}
handleChange = input => event => {
this.setState({ [input] : event.target.value })
}
render(){
const {step} = this.state;
const { projectNumber, projectName, numberOfSystems, buildSheet , controlPhilosophy, projectLayoutDrawing, projSoftwareValidation, CppDrawing, radioGroup21 } = this.state;
const values = { projectNumber, projectName, numberOfSystems, buildSheet, controlPhilosophy, projectLayoutDrawing, projSoftwareValidation, CppDrawing, radioGroup21 };
switch(step) {
case 1:
return <ProjectInfo
nextStep={this.nextStep}
handleChange = {this.handleChange}
values={values}
/>
case 2:
return <PrelimInspection
nextStep={this.nextStep}
prevStep={this.prevStep}
handleChange = {this.handleChange}
values={values}
/>
export default MainForm;
-----------------------------------
//Child Component
import React, { Component } from 'react';
import { Form, Button, Radio } from 'semantic-ui-react';
import { throws } from 'assert';
class PrelimInspection extends Component{
saveAndContinue = (e) => {
e.preventDefault();
this.props.nextStep();
}
back = (e) => {
e.preventDefault();
this.props.prevStep();
}
render(){
const { values } = this.props
return(
<Form color='blue' >
<h1 className="ui centered">System Installation</h1>
<Form.Field inline>
<Form.Field>System Properly Supported</Form.Field>
<Radio
label = {'Yes'}
name = {'radio21'}
value = {'Yes'}
onChange={this.props.handleChange('radioGroup21')}
defaultValue={values.radioGroup21}
/>
<Radio
label = {'No'}
name = {'radio21'}
value = {'No'}
onChange={this.props.handleChange('radioGroup21')}
defaultValue={values.radioGroup21}
/>
</Form.Field>
<Button onClick={this.back}>Back</Button>
<Button onClick={this.saveAndContinue}>Save And Continue </Button>
</Form>
)
}
}
export default PrelimInspection
The app is rendering and the layout is correct. Unfortunately the state values aren't being sent to the parent state.
I checked the documentation https://react.semantic-ui.com/addons/radio/#types-radio-group and I have found few things you missed:
1.) Radio component asked the checked props (but you did not supply it).
2.) Which then requires you to pass the value, in your case it should come from the parent component:
<PrelimInspection
valueFromParent={this.state["radioGroup21"]}
nextStep={this.nextStep}
handleChange={this.handleChange}
values={values}
/>
so in your Child Component' render, take the value:
render() {
const { values, valueFromParent } = this.props;
...
3.) Radio's onChange value is passed as the second param (obj.value).
<Radio
label={'Yes'}
name={'radio21'}
value={"Yes"}
checked={valueFromParent === 'Yes'}
onChange={this.props.handleChange("radioGroup21")}
...
/>
So you can take the selected value like this:
// MainForm
handleChange = input => (event, obj) => { // obj is the second param
console.log("sendin here", input, obj.value);
this.setState({ [input]: obj.value });
};

Using React state to allow for temporary invalid numeric input

I may be thinking about this wrong but I would like to have two components, a parent and a child, where the child has an input element where the user is expected to enter a number. The parent component's callback will only get called on valid numbers. This is enforced via Typescript and parseFloat.
class Child extends React.Component {
state = { number: '' };
inputChanged = (ev) => {
const stringy = ev.target.value;
this.setState({number: stringy});
const parsed = parseFloat(stringy);
if (parsed) {
this.props.numberChanged(parsed);
}
}
render() {
return <input
type="text"
value={this.props.number || this.state.number}
onChange={this.inputChanged}
/>
}
}
class Container extends React.Component {
state = { number: 5 };
numberSet = (value: number) => {
this.setState({number: value});
}
render() {
return <Child
number={this.state.number}
numberChanged={this.numberSet}
/>;
}
}
The challenge comes in that I want to be able to set and change the number value in the child component with props. But I also want to allow the user to type numbers in.
If I only rely on the props and not state then when they type things like decimal points they do not show up because they are invalid from the perspective of parseFloat. But if I only rely on state then I can't seem to get the parent component to update the child component with new props.
I have created the following CodePen to reproduce as well. Notice how you cannot type a decimal point in the input field. Where am I going wrong here?
You are complicating things here with the pattern. The question itself may be incorrect and mis-informed.
But to address your problem of not able to use decimal values,
the problem is parseFloat('52.') is 52 and the code runs on every change because of the change in this.props and you are never successful in adding that '.' !
Using a controlled component like below should solve all your problems!
class Child extends React.Component {
render() {
return <input
type="text"
value = {this.props.number}
onChange={this.props.numberChanged}
/>
}
}
class Container extends React.Component {
state = { number: 5 };
numberSet = (ev) => {
let value = event.target.value;
this.setState({number: value});
}
render() {
return <Child
number={this.state.number}
numberChanged={this.numberSet}
/>;
}
}
React.render(<Container />, document.getElementById('app'));`

React Parent component checkbox state updates with one step delay

I have a Parent component:
import React, { Component } from "react";
import { Button } from "./Button";
export class Dashboard extends Component {
constructor(props) {
super(props);
this.state = {
numbers: [],
disabled: false
};
this.setNum = this.setNum.bind(this);
}
setNum(num) {
if (!this.state.numbers.includes(num)) {
this.setState(prevState => ({
numbers: [...prevState.numbers, num]
}));
} else if (this.state.numbers.includes(num)) {
let nums = [...this.state.numbers];
let index = nums.indexOf(num);
nums.splice(index, 1);
this.setState({ numbers: nums });
console.log(this.state.numbers);
}
if (this.state.numbers.length >= 4) {
this.setState({ disabled: true });
} else if (this.state.numbers.length < 4) {
this.setState({ disabled: false });
}
}
render() {
return (
<div className="board-container">
<div className="board">
<div className="row">
<Button
id="1"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
<Button
id="2"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
<Button
id="3"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
<Button
id="4"
numbers={this.state.numbers}
onChange={this.setNum}
disabled={this.state.disabled}
/>
</div>
</div>
</div>
);
}
}
... and a Child component:
import React, { Component } from "react";
export class Button extends Component {
constructor(props) {
super(props);
this.state = {
isChecked: false
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.setState({
isChecked: !this.state.isChecked
});
var num = e.target.value;
this.props.onChange(num);
}
render() {
const { isChecked } = this.state;
if (isChecked === true) {
var bgColor = "#f2355b";
} else {
bgColor = "#f7f7f7";
}
let disabled = this.props.disabled;
if (this.props.numbers.includes(this.props.id)) {
disabled = false;
}
return (
<div className="number-container" id="checkboxes">
<label
className={!isChecked && disabled === false ? "num" : "checked-num"}
style={{ backgroundColor: bgColor }}
>
{" "}
{this.props.id}
<input
type="checkbox"
name={this.props.id}
value={this.props.id}
id={this.props.id}
onChange={this.handleChange}
checked={isChecked}
disabled={disabled}
/>
</label>
</div>
);
}
}
Whenever any Button component is clicked, the Parent component gets the child Button's id value and puts it into its numbers state array. Whenever a Button is unchecked, the Parent updates is numbers state by removing the id of the child Button.
If my code is right, the expected behavior is whenever a Button checkbox is clicked, the Parent numbers state will be updated immediately (adding or removing a number). However, it always updates with one step lag behind.
I know, that the issue is dealing with the React states not being updated instantly, and I've checked similar issues on Stackoverflow. The problem is that I can't figure it out how to make this two components interact with each other in a proper way. What would be the solution for this issue?
Here are three screenshots from codesandbox
If you want to play with it please find the link https://codesandbox.io/s/w2q8ypnxjw
What I did was, I basically copied and pasted your code and updated setNum function to reflect the changes Think-Twice suggested
setNum(num) {
if (!this.state.numbers.includes(num)) {
this.setState(
prevState => ({
numbers: [...prevState.numbers, num]
}),
() => {
console.log("state logged inside if", this.state.numbers);
}
);
} else if (this.state.numbers.includes(num)) {
let nums = [...this.state.numbers];
let index = nums.indexOf(num);
nums.splice(index, 1);
this.setState({ numbers: nums }, () => {
console.log("state logged inside else if", this.state.numbers);
});
}
if (this.state.numbers.length >= 4) {
this.setState({ disabled: true });
} else if (this.state.numbers.length < 4) {
this.setState({ disabled: false });
}
}
So before going further let's quickly address a couple of things regarding to React and setState
As B12Toaster mentioned and provided a link which contains a
quote from official documentation
setState() does not always immediately update the component. It may
batch or defer the update until later.
Think-Twice's also points out that by stating
Basically setState is asynchronous in React. When you modify a value
using setState you will be able to see the updated value only in
render..
So if you want to see the immediate state change in a place which
you trigger setState, you can make use of a call back function as
such setState(updater[, callback])
There are two approaches when it comes to and updater with setState,
you could either pass an object, or you could pass a function So in
Think-Twice's example, an object is passed as an updater
this.setState({ numbers: nums } //updater, () => {
console.log(this.state.numbers); //this will print the updated value here
});
When a function is used as an updater (in your setNum function you
already do that), the callback function can be utilized like below
if (!this.state.numbers.includes(num)) {
this.setState(
prevState => ({
numbers: [...prevState.numbers, num]
}),
() => {
console.log("state logged inside if", this.state.numbers);
}
);
}
Your current implementation and communication structure seems fine. It is actually called Lifting State Up which is recommended also by official documentation.
Basically you store the state of array numbers in a parent component (which can be considered as the source of truth) and you pass the method that changes the state as a prop to it's child component.
In the codesandbox link I provided, the functionalities works the way I expect (at least this is what I expect from your code)
Basically setState is asynchronous in React. When you modify a value using setState you will be able to see the updated value only in render. But to see updated state value immediately you need to do something like below
this.setState({ numbers: nums }, () => {
console.log(this.state.numbers); //this will print the updated value here
});

Reactjs Select v2 - How to handle Ajax Typing?

I am using reactjs select 2 but I don't know how to make it work so that when a user types something in a ajax request is made and the results are sent back.
I see it has some async options but I don't get how it works and how I would get it to work with axios.
I come up with this but it is kinda laggy when a user types(probably because it is re-rendering it after each type) and when the user selects a choice the value disappears.
export default class TestComponent extends Component {
constructor(props) {
super(props);
this.state = {value: ""};
}
onInputChange(option) {
this.getOptionsAsync(option)
}
getOptionsAsync(newInput) {
var that = this;
console.log("ffd", newInput)
axios.get(`https://localhost:44343/api/States/GetStatesByText?text=${newInput}`)
.then(function (response) {
var formatedResults = response.data.map((x)=> {
return {value: x.id, label: x.name}
})
that.setState({
options: formatedResults,
value: newInput
})
})
.catch(function (error) {
});
}
render() {
console.log(this.state.value, "value")
return (
<div className="test">
<Select
onInputChange={this.onInputChange.bind(this)}
value={this.state.value}
options={this.state.options }
/>
</div>
);
}
}
You're going to be doing an api call every single time that you type a letter with the current way you're doing things. I would recommend just loading the states once at the beginning, perhaps in your ComponentDidMount() method.
If you pass the isSearchable prop to React-Select it will automatically work as a filter anyways.
Another thing I've had to do in this case which I believe will fix your change problem is to make sure it calls the handler on change not just on input change.
Pass this prop:
<Select
value={this.state.value}
options={this.state.options }
onChange={value => {
if (value) this.onInputChange(value)
else this.onInputChange('')
}
/>
Due to the way this is automatically bound to arrow functions, you won't have to bind to this if you change your onInputChange to the following:
onInputChange = (value) => {
this.getOptionsAsync(value)
}
Finally, you should be setting the state in the above function so the value is stored.
onInputChange = (value) => {
this.getOptionsAsync(value)
this.setState({value})
}

Resources