I am trying to change an input inside a GrandChild class and a Bootstrap Table inside Parent class*. An user would change the input inside **GrandChild class then save it, so the changes are seen in the Bootstrap Table in Parent class; however, I am seeing this weird behavior where my props are changing before I call the .onChange (which is my save). I believe this is causing my inputs to not save or setting the state properly.
Data being passed down hierarchy: GrandParent => Parent => Child => GrandChild
It is occurring at the Child class's handleSave() function:
export class Child extends React.Component {
constructor(props){
this.state = {
data:this.props.data
}
}
handleChange = (name, value) => {
this.setState((prevState) => {
let newState = {...prevState};
newState.data.dataList[0][name] = value; // data
return newState;
});
};
handleSave(){
let dataList = this.state.data.dataList.slice();
console.log("dataList state-dataList:", dataList);
console.log("dataList before onChange 2:", this.props.data.dataList); //Same as dataList or this.state.data.dataList
this.props.onChange("dataList", dataList);
console.log("dataList onChange 3:", this.props.data.dataList); //Same as dataList or this.state.data.dataList
}
render() {
return (
<div>
<GrandChild data={this.state.data} onChange={this.handleChange} />
</div>
)
}
Child class's this.props.onChange gets sent back to the Parent class:
export class Parent extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
columns = [
{dataField: '..', text: '...' },
{dataField: '..', text: '...' },
{dataField: '..', text: '...' },
{dataField: '..', text: '...'}];
handleChange = (name, value) => {
this.props.onChange(name, value);
};
render() {
return (
<div>
<BootstrapTable
hover
condensed={true}
bootstrap4={true}
keyField={'id'}
data={this.props.data.dataList}
columns={this.columns}
/>
<Child data={this.props.data} onChange={this.handleChange} />
</div>
);
}
}
Then Parent class's this.props.onChange* gets sent to GrandParent Class:
export class GrandParent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: {...this.props.location.state.data}
};
this.handleChange = this.handleChange.bind(this);
}
handleChange = (name, value) => {
this.setState((prevState) => {
let newState = {};
let data = Object.assign({}, prevState.data);
data[name] = value;
newState.data = data;
return newState;
});
};
render() {
return (
<div>
<Form>
<Parent data={this.state.data} onChange={this.handleChange} />
</Form>
</div>
)
}
This is the GrandChild's class:
export class GrandChild extends React.Component {
constructor(props) {
super(props);
this.handleInputChange = this.handleInputChange.bind(this);
}
handleInputChange = (event) => {
const target = event.target;
const value = target.type === 'checkbox' ?
target.checked :
target.value;
const name = target.name;
this.props.onChange(name, value);
};
render() {
return (
<div>
<Form.Row>
<Form.Group as={Col}>
<Form.Label>Label Name</Form.Label>
<Form.Control name="labelName" value={this.props.data.[0]labelName || ""} //ignore the index for now
onChange={this.handleInputChange}/>
</Form.Group>
</Form.Row>
</div>
)
}
}
I expected that console.logs() of the dataLists to be different; however, they give the same exact object before it even runs the this.props.onChange("dataList", dataList);
Potentially, the third dataList console.log might be same as the state dataList because of setState being asynchronous.
It looks like the main issue is that you're mutating state/props in Child:
handleChange = (name, value) => {
this.setState((prevState) => {
// {...prevState} makes a shallow copy of `prevState`
let newState = {...prevState};
// Next you're modifying data deep in `newState`,
// so it's mutating data in the `dataList` array,
// which updates the source of truth for both props and state.
newState.data.dataList[0][name] = value;
return newState;
});
};
One way to do this (avoiding mutation) is like this:
handleChange = (name, value) => {
this.setState(prevState => ({
data: {
...prevState.data,
dataList: [
{
...prevState.data.dataList[0],
[name]: value
},
...prevState.data.dataList.slice(1)
]
}
}));
};
If that's more verbose than you'd like, you could use a library like immutable-js.
Another issue that could cause you bugs is copying props into state. This article gives some explanation of why that's bad: https://overreacted.io/writing-resilient-components/#dont-stop-the-data-flow-in-rendering
Basically: If you set props in state and then update state and pass props down to a child, the data you're passing down will be stale. It doesn't look like you're doing that here, but it would be easy to miss. An easy way to avoid this is to name any props you plan on setting in state initialProp If your prop is named initialData, it will be clear that from that point in the tree you should rely on the value in state rather than props.
Also, handleChange in Grandparent can be written more simply:
handleChange = (name, value) => {
this.setState(prevState => ({
data: {
...prevState.data,
[name]: value
}
}))
};
Related
Ok, I'm new to react and mobx, and I'm experiencing some issues to manipulate the store.
When I'm typing at the input, the value gets overwritten for each char typed.
The component:
#withStore
#observer
class ConfigModel extends Component {
configModel;
constructor(props) {
super(props);
this.configModel = this.props.store.configModelStore;
}
render() {
const fieldsObj = this.configModel.modelConfig;
const fieldHelpers = this.configModel.helperModelStore.modelConfig;
const callbackOnChange = this.configModel;
const campos = merge(fieldHelpers, fieldsObj); // _.merge()
return (
<Form key={'configModelForm'}>
<>
{Object.entries(campos).map((campo) => {
if (campo[1].advanced) {
return;
}
if (campo[1].type === 'input') {
return (
<InputRender
key={campo[1].id}
field={campo[1]}
onChange={callbackOnChange.valueOnChange}
/>
);
}
})}
</>
</Form>
);
}
}
And my store define some observables (some options were omitted for simplicity, like the type evaluated at the component above):
#observable modelConfig = [{
id: 'postType',
value: '',
disabled: false,
advanced: false,
},
{
id: 'pluralName',
value: '',
disabled: false,
advanced: true,
},
...
]
And also define some actions:
#action valueOnChange = (e, {id, value}) => {
this.modelConfig.filter((config, index) => {
if (config.id === id) {
this.modelConfig[index].value = value;
console.log(this.modelConfig[index].value);
}
});
The console.log() above prints:
I truly believe that I'm forgetting some basic concept there, so can someone spot what am I doing wrong?
*EDIT:
I have another component and another store that is working correctly:
#observable name = '';
#action setName = (e) => {
this.name = e.target.value;
console.log(this.name);
}
So my question is:
Why the action that targets a specific value like this.name works fine and the action that targets a index generated value like this.modelConfig[index].value doesn't works?
The problem was at the <InputRender> component that was also receiving the #observable decorator. Just removed and it worked.
// #observer <---- REMOVED THIS
class InputRender extends Component {
render() {
const item = this.props.field;
return (
<InputField
id={item.id}
label={
<InfoLabel
label={item.label}
action={item.action}
content={item.popupContent}
/>
}
placeholder={item.placeholder}
onChange={this.props.onChange}
value={item.value}
disabled={item.disabled}
error={item.error}
throwError={item.throwError}
/>
);
}
}
I want to access the state of a Child component by using refs, but the state of the ref is always null.
In my React app, I have an Editor(basically, it is a form) that manipulates its own states, e.g. value change, update. The editor is used on multiple pages.
Editor.jsx
export default class Editor extends React.Component {
constructor(props) {
super(props);
this.state = {
value1: null,
... other values
};
}
onValue1Change = (e) => {
this.setState({value1: e.target.value});
}
onSave = (e) => {
// save values
}
render() {
return (
<div>
<input value={this.state.value1} onChange={this.onValue1Change}/>
... other input fields
<button onClick={this.onSave}>Save</button>
</div>
)
}
}
Now, there is a RegisterForm which covers all fields in the Editor. I made a small change in the Editor to hide the Save button so I can use it in the RegisterForm:
RegisterForm.jsx
export default class RegisterForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: null,
firstname: null,
lastname: null
};
this.Editor = React.createRef();
}
onSave = (e) => {
let childState = this.Editor.current.state;
// childState is ALWAYS null!
}
render() {
return (
<div>
<input value={this.state.email} onChange={this.onEmailChange}/>
<input value={this.state.firstname} onChange={this.onFirstnameChange}/>
<input value={this.state.lastname} onChange={this.onLastnameChange}/>
...
<Editor ref={this.Editor} showSave={false}/>
...
<button onClick={this.onSave}>Save</button>
</div>
)
}
}
Turns out this.Editor.current.state is always null.
I have two questions.
Why this.Editor.current.state is null?
If I want to use props, how should I change my code? E.g. If I let RegisterForm pass props to Editor, I'd imagine something like this:
Editor.jsx
export default class Editor extends React.Component {
// same constructor
onValue1Change = (e) => {
this.setState({value1: e.target.value}, () => {
if(this.props.onValue1Change) this.props.onValue1Change(e);
});
}
// same render
}
RegisterForm.jsx
export default class RegisterForm extends React.Component {
constructor(props) {
super(props);
this.state = {
email: null,
firstname: null,
lastname: null,
value1: null,
};
}
onValue1Change = (e) => {
this.setState({value1: e.target.value});
}
render() {
return (
<div>
<Editor showSave={false} onValue1Change={this.onValue1Change}/>
...
</div>
)
}
}
does it make the Child component render twice? Any suggestions on how to improve it?
You are passing the ref as a prop to the <Editor/> component but not doing anything with it after that.
For example:
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// You can now get a ref directly to the DOM button:
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
Receive props and ref through the forwardRef() callback parameter, then pass the ref to the child node.
This is called ref forwarding
I made a code sandbox for you to test it!
What I am trying to do is:
on the parent component I have onChange=this.handleChange that it works fine for textInputs and it allows me to go to the next steps of my form but it doesn't setState of dropDowns whatever I do my dropdowns are empty and if I setState dropdowns on the child I am unable to trigger the handleChange on the parent thing that I need
class CompanyInfo extends React.Component {
constructor(props) {
super(props);
this.state = {
value: this.props.company,};
this.handleChange = this.props.onChange.bind(this);
}
render() {
return(
<SelectInput value={this.state.value}
onChange={this.handleChange}
floatingLabelText={messages.company[lang]}
floatingLabelFixed={true}
>
{this.props.company.map((element) => {
return <MenuItem value={element.Value} primaryText={element.Value} />})}
</SelectInput>
parent:
handleChange = (event) => {
console.log("test");
const { stepIndex } = this.state;
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
var type = null;
switch(stepIndex) {
case 0: type='UPDATE_CINFO'; break;
case 1: type='UPDATE_PINFO'; break;
default: break;
}
this.props.dispatch(updateCurrentForm( type, { [name]: value } ));
this.setState( this.state , () => this.validateFields() )
}
Just use the prop of handleChange directly in the child component. And you also should control the state only at one place and use the state values as props in child components.
render() {
const {handleChange, company} = this.props
return(
<SelectInput value={company}
onChange={handleChange}
floatingLabelText={messages.company[lang]}
floatingLabelFixed={true}
>
...
}
I am trying to make an edit page. I am update the state for any changes made. I want to compare the initial state with the last state on the last save. but I can not control the first state.
export default class extends Component {
constructor(props){
super(props);
this.changeDetails = this.changeDetails.bind(this);
this.state = {
driver: this.props.driver
}
}
changeDetails = value => {
this.setState({
driver:value
})
}
onRegister = () => {
//I want to make a comparison here.
}
render() {
const {driver} = this.state
return (
<div>
<EditView driver={driver} changeDetails={this.changeDetails}/>
</div>
);
}
}
EditView.js
export default class extends Component {
render() {
const { driver} = this.props;
const changeDetails = event => {
driver['fname] = event.target.value;
this.props.changeDetails(driver);
};
return (
<div>
<Input
value={driver.fname}
onChange={event => changeDetails(event)}
/>
</div>
);
}
}
Do not mutate driver itself directly. Use something like this:
const changeDetails = event =>
this.props.changeDetails( { ...driver, fname: event.target.value } );
Hi Im trying to display the array with textbox for each element as shown in the image. The issue faced by me is when I enter the new name in textbox the same name is assigned for all the elements, so how do I overcome this issue, and save individual name for each current name. So this help in updating the database with new name.
class TablebackupName extends Component {
constructor(props) {
super(props);
this.state = {
tName: [pokemon, XXX, Batman],
bName: [newname : ''],
};
this.onNameEdited = this.onNameEdited.bind(this);
}
onNameEdited(event) {
this.state.bName.newname = event.target.value;
this.setState({ bName: this.state.bName });
};
render() {
return (
<div>
{this.state.tName.map(x =>
<input type="text" label={x} key={x.toString()} value={this.state.bName.newname} onChange={this.onNameEdited} />)}
</div>
);
}
}
Don't mutate state directly:
//wrong
this.state.bName.newname = event.target.value;
this.setState({ bName: this.state.bName });
//right
this.setState({ bName: {newName: event.target.value} });
You set the same state property for all of your map elements, so the state is shared.
I'd tackle it by setting defaultValue and then - on each update, just update the tName array with the new values.
export default class TablebackupName extends React.Component {
constructor(props) {
super(props);
this.state = {
tName: ["pokemon", "XXX", "Batman"]
};
this.onNameEdited = (index) => (event) => {
//using map to keep everything immutable
let element = event.target;
this.setState({
tName: this.state.tName.map((val, i) => i === index ? element.value : val)
})
};
}
// creating a higher order function to get the right index for later use.
render() {
return (
<div>
{this.state.tName.map((x, index) => {
return (
<div key={index}>
<label>{x}</label>
<input type="text" defaultValue="" onKeyUp={this.onNameEdited(index)}/>
</div>)
})}
</div>)
}
}
webpackbin: https://www.webpackbin.com/bins/-Ko7beSfn-FR__k94Q3k