ReactJS: Maintain data state between parent and child - javascript

There's a parent, <MessageBox /> element, which contains a list of messages stored in its state. For each message in messages, a <Message /> element is created inside <MessageBox /> which has fields for message.subject and message.body. A user can edit the message.subject and message.body and once done, the message object is sent back to <MessageBox /> through a props.updateHandler() to maintain the message state in the parent.
In my current approach, I'm storing the message data in MessageBox's state and in the render() function, I'm creating the <Message /> elements and passing a callback to each of them to send back data changes. In the callback, the updated data from each of the <Message /> elements is updated back into MessageBox's state. The reason for this is to keep all the recent updated data in one place only. The above approach creates havoc if shouldComponentUpdate() method in <Message /> is not overloaded (infinite recursion).
Is there a better approach for this? I've to write a lot of code just to override the builtin methods to keep the entire thing stable. As I'm not planning to go for Flux/Redux, is there a React-only approach for this?
EDIT: Since there's a lot of confusion, I'm adding minimal code.
class Message extends React.Component {
constructor(props) {
this.state = {
subject: this.props.subject,
body: this.props.body,
type: this.props.type,
messageIndex: this.props.messageIndex
};
}
componentDidUpdate() {
this.props.updateHandler(messageIndex, {
subject: this.state.subject,
body: this.state.body,
type: this.state.type
});
}
render() {
return (
<div>
<input
type="text"
defaultValue={this.state.subject}
onBlur={e => this.setState({subject: e.target.value})} />
<input
type="text"
defaultValue={this.state.subject}
onBlur={e => this.setState({body: e.target.value})} />
<select
type="text"
value={this.state.subject}
onChange={e => this.setState({type: e.target.value})}>
<option>Type 1</option>
<option>Type 2</option>
</select>
</div>
)
}
}
class MessageBox extends React.Component {
constructor(props) {
this.state = {
messages: aListOfMessageObjects
}
}
updateHandler(message) {
// Message update happens here and returns a list updatedMessages
this.setState({
messages: updatedMessages
});
}
render() {
let _this = this;
var messagesDOM = this.state.messages.map((m) => {
return (
<Message
message={m}
updateHandler={_this.updateHandler.bind(_this)} />
);
})
return (
<div>
{messagesDOM}
</div>
);
}
}

If that can help, read thinking-in-react. It explains how data should go only one way to avoid be lost in UI updates.
React ToDo MVC will provide you an example of React good practice on a real case
To know how to pass props from your parent to children read controlled-components. you'll have to use value and onBlur on each input. Any onBlur event will call this.props.updateHandler with e as parameter instead of e => this.setState({type: e.target.value}).

Don't do a callback to MessageBox from componentDidUpdate() of Message. Do a callback directly from an action in Message.
You don't need state in Message component at all. Props will keep the values you are interested if you update parent's state properly.
What you need is something like:
<input type="text"
defaultValue={this.props.subject}
onBlur={e => this.updateSubject(e.target.value)} />
updateSubject: function(newSubjectValue) {
this.props.updateHandler(messageIndex, {
subject: newSubjectValue,
body: this.props.body,
type: this.props.type
});
}
That way the component will get re-rendered, but won't do another call to the parent's setState.

Related

I am updating my state using this.setState but still i am recieving the old values in render()

class EditLocation extends Component {
constructor(props) {
super();
this.state = {
LocationId: '',
locationOptions: [],
}
this.baseState = this.state;
this.findLocationById = this.findLocationById.bind(this);
}
findLocationById = (locationId) => {
let locationOptions = [];
if (locationId <= 0) {
setTimeout(()=>{
this.setState(this.baseState);
locationOptions.push(
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={false}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>
)
this.setState({locationOptions:locationOptions})
},200)
else {
setTimeout(() => {
let location = this.props.store.selectedLocation;
this.props.store.LocationTypes.forEach((value)=>{
if(location.LocationTypes ?
location.LocationTypes.includes(value.LocationTypeId): false)
{
locationOptions.push(
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={true}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>
)
}
else
{
locationOptions.push(
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={false}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>
)
}
})
this.setState({
LocationId: location.LocationId,
locationOptions: locationOptions,
})
render(){
return (
<div>
<Modal>
<Form>
<FormGroup>
<input
value={this.state.LocationId}
type="text"
name="Location"
id="Location"
/>
</FormGroup>
<FormGroup>
{console.log(this.state.locationOptions)} // showing updated state value
{this.state.locationOptions} // showing previous state.locationOptions value
</FormGroup>
</Form>
</Modal>
</div>
)
}
}
console.log() inside the render is updating the value by my checks on customInput are not updating. I need to either reopen the modal or reload the whole program to see updates.
Any solution and resources would be helpful as I am stuck at it for hours and can't seem to figure the issue. and store is mobx store if that helps
You using setState in a wrong way.
setState() enqueues changes to the component state and tells React
that this component and its children need to be re-rendered with the
updated state. This is the primary method you use to update the user
interface in response to event handlers and server responses.
React docs
So it asynchronous and you can't guarantee when update happens.
Using setTimeout is a bad manners in React.
Storing whole components in state, e.g. locationOptions isn't good idea either.
Better to move input to separate component, as i see only defaultChecked different.
Better to use Hooks, easier to think about this in React way, it's require some time and effort to figure out how to write declarative instead of imperative code.
Can refactor a litle
// sync local state and props is a pain, better to avoid it
//constructor(props) {
//super(props);
//const location = this.props.store.selectedLocation
// this.state = {
//LocationId: location.LocationId,
//}
//}
const location = this.props.store.selectedLocation;
render() {
return (
<div>
<Modal>
<Form>
<FormGroup>
<input
value={this.props.store.selectedLocation}
type="text"
name="Location"
id="Location"
/>
</FormGroup>
<FormGroup>
{this.props.store.cationTypes.map((value) => (
<CustomInput
type="checkbox"
id={value.LocationTypeId}
key={value.LocationTypeId}
value={value.LocationTypeId}
defaultChecked={location.LocationTypes.includes(value.LocationTypeId)}
label={`${value.LocationTypeName}`}
className="mb-0"
onChange={this.handleCheckbox.bind(this)}
/>}
))
</FormGroup>
</Form>
</Modal>
</div>
);
}

Submitting form with textarea in React

I have a textarea that I want to stringify to JSON on form submission. I will even settle for just having the function set the textarea value.
import React from 'react';
export default class ClinicalMain extends React.Component {
constructor(props) {
super(props);
}
state = {selectedOption: ''}
// my function to update the textarea
reactStringify() {
let obj = {
name: "bob",
age: 4
}
console.log('in stringify');
let value = JSON.stringify(obj);
}
componentDidMount() { }
render() {
return (
<React.Fragment>
<form>
<button type="button"
onClick={this.reactStringify}
id="reactid"
>React stringify</button>
<textarea value={this.value}
defaultValue=""
rows="10" cols="80"
></textarea>
<br />
</form>
</React.Fragment>
)
}
}
let value does not update. Do I need to use setState? this?
There are a number of issues in the code indicating a lack of familiarity with the excellent React tutorial. As with any library, it's necessary to spend time reading the manual before diving in.
State should not be modified directly. Use this.setState() to replace state. this.setState() doesn't work instantly; it simply informs the React library that the state needs updating and React handles the update on its own when it sees fit.
Beyond this, let value = ... is a purely local variable, not a class variable, so this.value would be undefined in render no matter what; in other words, your code doesn't attempt to modify or access state in any way.
Class functions that attempt to access this need to be bound. For example, onClick={this.reactStringify} passes a reference to the this.reactStringify function, but this will be undefined inside of this.reactStringify unless an arrow function is used (which implicitly binds this), or this is explicitly bound:
this.handleChange = this.handleChange.bind(this);
Explicit is considered to be better practice for class components than the arrow function approach because it only requires one call to bind when the component is constructed.
React typically uses something called controlled components to listen to changes on a text field. This means that the element's value tracks component state and acts as the single source of truth.
While I'm not exactly sure what you're ultimately looking to accomplish, here's a working example to get you moving again which demonstrates the above concepts.
class ClinicalMain extends React.Component {
constructor(props) {
super(props);
this.state = {value: "", output: ""};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange({target: {value}}) {
this.setState(() => ({value}));
}
handleSubmit(e) {
e.preventDefault();
this.setState(state => ({
output: `you wrote: "${state.value}"`
}));
}
render() {
return (
<React.Fragment>
<form onSubmit={this.handleSubmit}>
<textarea
value={this.state.value}
onChange={this.handleChange}
></textarea>
<div>
<input type="submit" value="Show output" />
</div>
</form>
<div>{this.state.output}</div>
</React.Fragment>
);
}
}
ReactDOM.createRoot(document.querySelector("#app"))
.render(<ClinicalMain name="World" />);
<script crossorigin src="https://unpkg.com/react#18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#18/umd/react-dom.development.js"></script>
<div id="app"></div>
Here are relevant sections of the documentation which provide more detail:
State and Lifecycle
Handling Events
Forms

React input binding to state vs local variable

I am looking at a tutorial for binding input in react with state. What I don't understand is why do I need to bind it to the state vs just a local vairable since it won't cause renders.
In my case I have a login form, in the tutorial it is sending message form. The idea is that the value is send to the App.js(parent) on submit using inverse data flow. It looks like this:
class Login extends Component{
constructor(){
super()
this.state = {
username: ''
};
this.login = this.login.bind(this)
this.handleChange = this.handleChange.bind(this)
}
handleChange(e) {
this.setState({
username: e.target.value
});
}
//here I make a post request and then I set the user in app.js
handleSubmit(e) {
e.preventDefault();
fetch('http://localhost:8080/login', {
method: 'post',
body: JSON.stringify(username)
}).then(data => data.json())
.then(data => {
this.props.setUser(data)
this.setState({
username: ''
})
}
}
render(){
return (
<section>
<form onSubmit={this.submit}>
<input placeholder="username"
onChange={this.changeInput} type="text"
value={this.state.username}/>
</form>
</section>
)
}
Is there a reason to use setState vs just a local variable which won't cause a rendering?
You don't have to, you could make it work without ever storing username in the state. All you have to do is listen for a submit event and fetch the input value at that time, using a ref.
class Login extends Component {
handleSubmit(e) {
e.preventDefault();
console.log(this.refs.username.value)
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<input ref="username" type="text" />
<button type="submit">Submit</button>
</form>
);
}
});
However the usual way to do that with React is to store the input value in the state and update the state every time the value change. This is called a controlled component and it ensures that the input value and the state are always consistent with each other.
class Login extends Component {
constructor() {
super()
this.state = {
username: ''
};
}
handleChange(e) {
this.setState({
username: e.target.value
});
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<input onChange={e => this.setState({ username: e.target.value })} type="text" value={this.state.username} />
<button type="submit">Submit</button>
</form>
</div>
)
}
}
Among other things, it makes things easier to validate the input value or modify it if you need. For instance, you could enforce uppercase by converting the state to uppercase whenever the state changes.
The value of the input field has to change to the new value, that is why for the on change event you set the state to the next event value. If you don't set state, the value will be the same even though it is entered by user.
Hope this helps,
Happy Learning.

How to pass data to a form in ReactJS?

How do you pass data to a form to edit in ReactJS?
I have 3 components, Table, Form and a parent component.
return (
<Table />
<Form />
);
Table renders a list of items and each row has an Edit button
So, when the Edit button is clicked, it will call an edit function (passed from parent using props) with the id of the item.
this.props.edit('iAmTheIdOfTheItem');
The edit function will set the id in the parent component state.
edit = (id) => {
this.setState({ id })
}
The selected item is passed to the Form component.
<Form item={ items.find(item => item.id === this.state.id) } />
This should store the current passed data in the Form component state. So that I can use that data to make any changes.
When the update button will be clicked, it will pass the state to the parent component.
Possible Solutions
componentDidMount
I can't set the state using componentDidMount since the Form component is already mounted.
id && < Form />
While this can help me use componentDidMount. But the problem is that the Form component is wrapped in a Modal component. So, closing animation will not work when I update the value and clear the id in the state.
getDerivedStateFromProps
I can use this one as a last resort but it looks like a hack to me and I'm not sure if I really need this. Since I've more than 5 forms, adding this every form does not look good to me.
Any better approach? Suggestions?
This should be helpful
https://codesandbox.io/s/82v98o21wj?fontsize=14
You can use Refs for this.
Reference the child using this.child = React.createRef() and pass this to your child component like this ref={this.child}. Now you can use that child's functions like this this.child.current.yourChildsFunction("object or data you want to pass") then on that child function, just set your form state using the data passed.
Here's a complete example. Sandbox HERE
class Parent extends Component {
constructor(props) {
super(props);
this.child = React.createRef();
}
onClick = () => {
this.child.current.fillForm("Jackie Chan");
};
render() {
return (
<div>
<button onClick={this.onClick}>Click</button>
<br />
<br />
<Child ref={this.child} />
</div>
);
}
}
class Child extends Component {
constructor(props) {
super(props);
this.state = {
name: ""
};
}
handleChange = ({ target }) => {
this.setState({ [target.name]: target.value });
};
fillForm(passedvalue) {
this.setState({ name: passedvalue });
}
render() {
return (
<form>
<label for="name">Name</label>
<input
id="name"
name="name"
onChange={this.handleChange.bind(this)}
value={this.state.name}
/>
</form>
);
}
}
Here an updated version of your sandbox: https://codesandbox.io/s/jprz3449p9
The process is quite basic:
You send the edited item to your form (via props)
The form stores a copy in its own state
When you change the form, it changes the local form state
On submit, you send the state to your parent with a callback (onUpdate here)
What you were missing:
Refresh the local form state when props change
if (!item || id !== item.id) {
this.setState(item);
}
Notify the parent when the item is updated with a callback
update = event => {
event.preventDefault();
this.props.onUpdate(this.state);
this.setState(emptyItem);
};

React doesn't re-render on props change

I am kinda new to react and to the webitself.
this is my render function
render() {
const {repositories} = this.props
return (
<div className='mt4 bt b--black-20 boardingbox scrollarea-content' style={{overflow: 'scroll', height: '100vh'}}>
{
repositories.map((repo, index) => {
console.log(repo.name)
return <Note name={repo.name} desc={repo.name} key={index} onClick={ this.handleClick.bind(this) }/>
})
}
</div>
)
}
The repositories is changing the way I want, but for some reason the its not get re-rendered. I passing the repositiores property from the parent.
The first time I render it (click to the search button, get a response from the server, and set the repo array), its working fine. But at the 2nd search, when there is something in the array, its not working properly, and not re-render.
UPDATE:
The parent's render / onClick
render() {
const {repositories} = this.state
return (
<div className='w-third navpanel br b--black-20'>
<SearchBar onClick={this.onClick} onChange={this.onChange}/>
<RepoList repositories={repositories}/>
</div>
//<NewNote />
//<Tags />
//<NoteList />
);
}
onClick = (event) => {
const {searchTerm} = this.state
let endpoint = 'https://api.github.com/search/repositories?sort=stars&order=desc&q=' + searchTerm;
fetch(endpoint)
.then(blob => blob.json())
.then(response => {
if(response.items)
this.setState({ repositories: response.items });
})
}
UP-UPDATE:
Search Comp:
constructor({onClick, onChange}) {
super()
this.onClick = onClick
this.onChange = onChange
this.state = {
imageHover: false
}}
render() {
return (
<div className='flex items-center justify-between bb b--black-20'>
<div className='ma2 inputContainer w-100'>
<input className='pa1 pl4 boardingbox w-100 input-reset ba b--black-20 br4 black-50 f6' placeholder='repos' type="text" onChange={this.onChange}/>
</div>
<div className='mr2'>
<div className='boardingbox pointer contain grow'>
<img src={(this.state.imageHover) ? NoteImageOnHover : NoteImage} alt=''
onMouseOver={()=>this.setState({imageHover: true})}
onMouseOut={()=>this.setState({imageHover: false})}
onClick={this.onClick}/>
</div>
</div>
</div>
)}
first responde
second responde
and I am really ashamed that I could screw up like this.
So basicly the problem was:
return <Note name={repo.name} desc={repo.name} key={index} onClick={ this.handleClick.bind(this) }/>
So I was as stupid to use INDEX as a KEY so I could not add again the same key to the array.
Thanks anyway guys! :)
The root cause most probably is due to error in function binding.
In your SearchComponent you are using the "props" to create function bindings in the contructor. This can cause your SearchComponent to refer to wrong instance of the functions for onClick and onChange. Would suggest referring to the official documentation for more details.
you do not need to rebind the functions in your SearchComponent, you can just use the functions received in props.
<input className='pa1 pl4 boardingbox w-100 input-reset ba b--black-20 br4 black-50 f6' placeholder='repos' type="text" onChange={this.props.onChange}/>
<!-- snipped other irrelevant code -->
<img src={(this.state.imageHover) ? NoteImageOnHover : NoteImage} alt=''
onMouseOver={()=>this.setState({imageHover: true})}
onMouseOut={()=>this.setState({imageHover: false})}
onClick={this.props.onClick}/>
Why could be happening to cause this behavior
Remember, constructor is only called once the component instance is being constructed, once it has been created and remains alive, React lifecycles take over.
So, when you first render your screen, the component is created and since there is only 1 of everything, it kind of works.
When you run your first search: onChange/onClick callbacks modify the state of the parent component. Which then calls render on the parent component.
At this point, it is possible that your SearchComponent maybe holding on to the wrong instance of the call back methods, which would thus not set state on the parent and thus not force re-render.
Additional Notes on your constructor
Normally you shouldn't refer to props in your constructor, but if you need to, then you need to have it in the format below. Here are the relevant docs:
constructor(props) {
super(props);
// other logic
}

Categories

Resources