React.js: Should parent or component manage state? - javascript

I have a "Date input" component that contains a <input type="text">.
It's purpose is to allow users to type in a valid date (e.g., mm/dd/yyyy).
Once a user enters a valid date, the parent of the component should receive that date.
Based on this - I'm trying to figure out whether the component or the parent should manage state.
If I set it up so the parent manages state:
render() {
return (
<div>
<input type="text" value={this.props.value} onChange={this.props.handleChange} />
</div>
);
}
... this is no good, because the parent will be notified with every single change, and the prop will be set to all the "draft" values (e.g., "07/2") while the user is typing.
That suggests that I should set this up so that the component manages it's own state:
class InputDateWithLabel extends Component {
constructor(props) {
super(props);
this.state = {
value: formatDate(this.props.value) // formatDate() formats a date object into an 'mm/dd/yyyy' string
};
this.handleChange = this.handleChange.bind(this);
this.handleBlur = this.handleBlur.bind(this);
}
handleChange(event) {
// use setState() to update this.state.value so the <input> shows what the user types
}
handleBlur(event) {
// if the user entered a valid date,
// convert the user's input to a date object,
// and communicate that to the parent. E.g.,:
this.props.onChange([new date]);
// Otherwise, handle the "error"
}
render() {
return (
<input type="text" value={this.state.value} onChange={this.handleChange} onBlur={this.handleBlur} />
);
}
}
This version works exactly the way I want it to, except for one more requirement...
Based on things that might happen elsewhere in my application, the parent may need to set the date in this component. However - now that the component is managing it's own state - when the parent changes props.value, my component will ignore it.
The React documents address this scenario here: You Probably Don't Need Derived State
But their solutions don't seem to apply:
One way to avoid the problems mentioned above is to remove state from our component entirely.
This is no good, because I don't want to make the parent responsible for validating the user's date input. My component should be self-contained, including the date validation, which means it needs to manage the "draft states" of the user's input.
Another alternative would be for our component to fully own the “draft” state. In that case, our component could still accept a prop for the initial value, but it would ignore subsequent changes to that prop
This is no good, because I need to retain the ability for the parent to change the value when appropriate.
The rest of the React documentation mentions a few other possibilities (getDerivedStateFromProps), but it goes to great lengths to stress that they're probably not correct. (Note the title of the article!)
This does not seem like an uncommon situation, so there must be a clear, simple, well-documented way to handle it, that's done the right "React-way". What is that?

Having a component manage it's own state doesn't seem that bad in your case, but you will need to add componentWillReceiveProps which adds another piece of code to manage.
componentWillReceiveProps(nextProps) {
this.setState({
value: formatDate(nextProps.value)
});
}

Related

React best practice for changing state of parent component from child without rerendering all children?

I have a project where I'm displaying cards that contain attributes of a person in a textfield, and the user can edit the textfield to directly change that person's attribute values. However every time they're editing the textfield it causes a rerender of all cards which slows down the app. Here is an example:
export default Parent() {
const [personList, setPersonList] = useState(/* list of person objects*/);
const modifyPerson(index, property, value) {
const newPersonList = _.cloneDeep(personList);
newPersonList[index][property] = value;
setPersonList(newPersonList);
}
const children = personList.map((person, index) => {
<Person
modifyPerson={modifyPerson}
index=index
/*properties on the person */
/>
});
return <div> {children} </div>
}
export default Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
}
So essentially when the child's text field is updated, it triggers a state change in the parent that stores the new value, then refreshes what the Child looks like. There's no button that the user clicks to "save" the values-- as soon as they edit the textfield it's a permanent change. And also the parent needs to know the new values of every Person because there's some functions that require knowledge of the current state of the person list. The Person component contains images that slow down the rendering if done inefficiently.
Is there a better way to make this design more performant and reduce rerenders? I attempted to use useCallback to preserve the functions but I don't it works in this specific design because the property and values are different-- do I have to create a new "modifyPerson" for each exact attribute?
Use React.memo()
React.Memo will check the props passed to the component and only if the props changes , it will re-render that particular component.
So, if you have multiple Person component which are getting props which are explicit to those Person, and when you change a particular Person component which leads to Parent state getting updated, only that Person component which you had modified will re-render because its props will change(assuming that you pass the changed value to it).
The other components will have the same props and wont change.
export default React.memo(Person(props) {
const fields = /* assume a list of these textfields for each property */
<TextField
value={props.name}
onChange={(e) => modifyPerson(props.index,"name",e.target.value)}
value={props.name} >
return {fields};
})
As others have already said React.memo() is the way to go here, but this will still re-render because you recreate modifyPerson every time. You can use useCallback to always get the same identity of the function, but with your current implementation you would have to add personList as dependency so that doesn't work either.
There is a trick with setPersonList that it also accepts a function that takes the current state and returns the next state. That way modifyPerson doesn't depend on any outer scope (expect for setPersonList which is guaranteed to always have the same identity by react) and needs to be created only once.
const modifyPerson = useCallback((index, property, value) {
setPersonList(currentPersonList => {
const newPersonList = _.cloneDeep(currentPersonList);
newPersonList[index][property] = value;
return newPersonList;
})
}, []);

React changing state in Parent , does render calls for all its children and sub-children as iteration?

i am setting the language name in my local storage , when it changes from a dropdown in topbar , i want the whole current view to be re-rendered and words translated to the selected language. my layout is like this
render(){
return (
<MainContainer>
<TopBar/>
<SideBar/>
<RouteInsideSwitch/>
</MainContainer>
)
}
in render of components ,the words to be translated basically calls a function that returns the correct word based on the local storage language name.
i change the language and i set the state in maincontainer for selected langauge and set it in local storage. however i dont want to move that state from Maincontainer to all my components. also dont want to store it in redux because then all the possible containers have to listen to it and then pass it to their children as props.
what currently happens is that saving state in mainContainer without passing it to any children , the children does re-render but only the immediate ones , if there are more children in those children and so on , it does not re-render because i m not passing the state throughout the chain.
open to any suggestion based on different pattern for language changing. but my question is that is there any way to re-render the current open view (all components in dom).
If your concern is that you have a number of "possible containers" which all need to handle the state change, perhaps consider creating a higher order component that includes the common language rendering logic (your RouteInsideSwitch leads me to believe this may the issue). In that way, you can avoid duplicating that logic across a ton of "possible" components that all require the functionality of dynamic language rendering and will avoid the need to dial a bunch of components into a redux store, assuming they are in the same hierarchy.
const DynamicLanguageComp = RenderComponent => {
return class extends Component {
constructor(props) {
super(props)
//additional state setup if needed
}
changeLangFunc = () => { /* handle change */ }
render() {
return <RenderComponent handleLanguageChange={this.changeLangFunc} {...this.props} {...this.state} />
}
}
}
If you would like to avoid a re-render on certain intermediate components that may be receiving props by way of state change you can implement the lifecycle method shouldComponentUpdate(), which by default returns true. You can make a comparison of nextProps to your current props, and return false if a re-render is undesired despite new props.

defaultValue of react not working in meteor

I am trying to figure this out, I don't know why this isn't working
<input type="text" id="first_name" name="first_name" className="form-control" defaultValue={this.props.user.first_name} required/>
but this works
<input type="text" id="first_name" name="first_name" className="form-control" value={this.props.user.first_name} required/>
the difference is value and defaultValue, if I use value the field becomes readonly and using defaultValue doesn't print any thing.
I am using react with meteor. I have tried logging this.props.user in render method before the return statement and it prints the object.
When you assign this.props.user.first_name to the value attribute it's not that the input field is becoming read-only, it's that you are never handling what happens when that value changes. React is simply re-rendering it with the value you directly assigned to it each time.
If you are looking to make the field editable + have the default user name value you should probably maintain and be aware of the state of the input.
So for example:
// Initialize some component state in either your constructor or getInitialState function
constructor(props){
super(props);
this.state = {userInput: this.props.user.first_name};
}
// Have a function that updates state when your input changes
onInputChange(event) {
this.setState({ userInput: event.target.value });
}
// Then set the value equal to your userInput state and add an onChange
// prop to the input tag in your render method.
render() {
return (
...
<input
type="text"
id="first_name"
name="first_name"
className="form-control"
value={this.state.userInput}
onChange={this.onInputChange.bind(this)} />
)
}
Then the value for the field initializes to the value it's being provided through this.props.user.first_name, while also remaining editable.
Edit:
As pointed out in the comments, while valid, this is actually an anti-pattern in React. Because the initial state of the child component is only called once, a change from a parent to the prop value of this.props.user.first_name will not cause any change in the state of the child. This is fine if the use case is to expressly set an initial value that you do not want or expect to change during the component life-cycle (though even then it's not a great pattern), but if you do expect the initial value to be mutable you have two options.
Option One: Bring the state up into the parent component, where it likely belongs. The child component should then receive and render any props that are sent it's way. Changes to the initial value are handled in the parent component state, props are treated as immutable, and everything stays in sync.
Option Two: If for whatever reason you both need to determine state from props and you also expect those props to change, you can make use of the componentWillReceiveProps(nextProps) life-cycle method to keep everything in sync. This will allow you to check this.props against nextProps and make any state changes if they are necessary:
componentWillReceiveProps(nextProps) {
if(nextProps.user.first_name !== this.props.user.first_name){
this.setState({
userInput: nextProps.user.first_name
});
}
}
Here's a link to the DOCS for further reference.

How do I change input field value the way it doesn't run through the whole Redux state update cycle every time it is changed?

I have a component that has an input field bound to the application state, like this
import {updateTitle} from '../actions/sales';
class Sale extends Component {
onTitleChange(event) {
const {value} = event.target;
const {id} = this.props;
this.props.updateTitle(id, value);
}
render() {
return (
<input
placeholder="The Title"
value={this.props.title}
onChange={::this.onTitleChange} />
);
}
}
export default connect(({sales}) => ({
title: sales.title
}), {
updateTitle
}))(Sale);
but a lot heavier in markup.
Everything is nice except that when I try to type something into this field fast, the browser lags significantly since every input change, which implies every key press, runs through the whole loop from event handler, to action creator, to the actual action, to the reducer, to the store update, to component props update and render. It's super slow. Is there any way to optimize that?
Am I missing something obvious?
I tried using debounced function passed as onChange prop value but, this way, the app state wouldn't update at all. I also tried using component-level state and setState along with app-level state but I think this approach contradicts the idea of Redux and therefore shouldn't be used.
React components can be controlled or uncontrolled. A controlled input receives its value as a prop and fires an event handler for each change in the value. An uncontrolled control keeps the user input in the local state and fires event handlers for changes.
Your input is currently controlled, but if you'd like to keep changes more local, why not make it uncontrolled? You can then call updateTitle in the onBlur event when the user is done typing, or debounce the onChange event to call updateTitle less often while the user is typing.
You have at least two options. The first: try connecting to the redux store closer to the input component (so less components that aren't affected by the value update on the change)
Generally though we store the intermediate values in the parent component state, and either flush the value occasionally (like a normal debounce) to the redux store, or do it on something like onBlur. So update the value in state on every change and flush the value to the store occasionally. It involves more care to make sure the values are in-sync but those are some of the trade-offs for optimizing hot paths.

Sync React state to Meteor collection using debounce

I have a textbox in my Meteor + React application. I want to sync its value to a Mongo collection. However, I don't want to update the collection after every keystroke, only when the user has stopped typing for a few seconds.
The textbox in my render() function looks like this:
<input type="text" ref="answer" onChange={this.onChange} value={this.state.someValue} />
I store the textbox value in this.state instead of this.data because this.data reflects the Mongo collection, which might have not been updated yet.
So far, all of this works.
The problem:
If another client updates the collection, I want the textbox to show the updated value. For this I have to update this.state inside the getMeteorData() function, but that's disallowed, and I get an error: "Calling setState inside getMeteorData can result in infinite loop".
Right now I have a workaround where I manually update the textbox value in componentDidMount() and getMeteorData(), but it feels hackish and I don't like it at all.
Is there a better way to do this? Can I maybe force state updates inside getMeteorData() if I promise I'll be a good boy and behave nicely?
I would get rid of getMeteorData at all and turn to createContainer. Data flow gets clear and simple most of the time, including this specific case. Here it goes.
First thing first, create a container to fetch data.
export default theContainer = createContainer(() => {
// Subscribe to the publication which publishes the data.
const subscription = Meteor.subscribe(...);
// Derive the data for the input box and form the props to pass down.
const props = {
answer: getAnswer(subscription)
};
return props;
}, theComponent);
theContainer acts as a container component and transferes the contained data to the presentational component theComponent by props. Be noted that the function given to createContainer is responsive, meaning that changes to reactive data sources in that function trigger rerun and result in rerender of theComponent.
By now we are all armed. Since data in the Mongo collection (Minimongo exactly) is synced by the props passed down, theComponent is aware of the synchronization by a prop transition.
export default class theComponent extends React.Component {
...
componentWillReceiveProps(nextProps) {
if (this.props.answer !== nextProps.answer) {
this.setState({
answer: nextProps.answer
});
}
}
render() {
return <input value={this.state.answer} onChange={this.onChange} />;
}
}
While such transition occurs, the upcoming value is updated to the state, and this controlled component will render the input based on the updated new value.
On the other hand, while the user starts typing, the change handler this.onChange updates the user's input to the state with every key stoke for this is a controlled component. However, the handler updates the Mongo collection (again, Minimongo exactly) only when the preset duration has elapsed to save data transmission.

Categories