I'm building an application that revolves around recording payments.
I have a state that's along the lines of
const [state, setstate] = useState({
amountPaid: Number(bill.total), // bill comes from another component fully populated
datePaid: new Date(),
});
I'm trying to render it in a MUI component (A modal)
return (
<div>
<form>
<TextField
type="number"
label="Enter the amount you are paying"
onChange={(e) =>
setState({ ...state, amountPaid: e.target.value })
}
value={state.amountPaid}
/>
When I submit the form, I update the bill as such, with the amount I paid (might be less than the whole amount needed)
useEffect(() => {
setPayment({
...payment,
amountPaid: Number(bill.total) - Number(bill.totalAmountReceived),
});
}, [bill]);
When I reopen the modal to check for the updated amount, the amountPaid row is correctly displayed according to the above logic. But the amountPaid is displayed as null initially, whereas the date is displayed correctly.
Why doesn't it initialize to bill.total?
Would be glad to provide any addition details that are needed.
You can only set component level initialization in any react component. If you want to set some value from previous/any parent component you will need to update the initial state value with the following expression:
If you are using a class based component the use componentDidMount(), if function based then use useEffect()
const[state,setState]=useState({
amountPaid: 0,
datePaid: new Date()
};
useEffect(()=>{
setState({...state,state.amountPaid:Number(bill.total) ,
//assuming bill is coming as a prop from another component
},[]);
One more suggestion is try not to use the state altering functions inside render, use a separate function and call state update logic in it
Related
I am using datepicker in a reactjs project but when i format the date with moment, the state is not being updated with the actual value that is selected
`
const [dates, setDates] = useState([])
const onDateChange = (values) =>{
setDates(values.map(item=>{
return moment(item).format("YYYY-MM-DD")
}))
console.log("dates:", dates);
}
<RangePicker
onChange={onDateChange}
/>
I'm not sure this is the problem but logging the value right after setState won't print the new state.
If you want to see the updated state you can create a useEffect function with the dates in the dependency array.
You can read more about it here https://beta.reactjs.org/reference/react/useState#troubleshooting
I am not quite sure what is the exact implementation of your <RangePicker .../> component. However I would like to point 3 important things out here, hope it will help :)
As <RangePicker .../> component is an input type, you would get its value from event that is passed as an argument to your onDateChange callback.
You are consoling the state at an event handler callback. This event handler callback will be sent to the input component and you would get the state lagging by 1 step. The reason it happens is the callback is prepared during initialisation of the React App when the state is empty. When you click it logs that state taken by it during initialisation and then state is set and re-rendering happens.
The state should be used as the value for your <RangePicker .../> component in order to make UI consistent. Make sure moment's date format is same as range picker date type.
In conclusion, it is not the moment that is causing issues. Hope if you verify and fix the above 3 it should work.
Please refer the example with an date input element:
import "./styles.css";
import { useState } from "react";
export default function App() {
const [date, setDate] = useState();
const onDateChange = (ev) => {
//1. Use event.target for state updates
setDate(ev.target.value);
/*2.1 Below console log goes with the input component,
This will take date what is currently set and not what we select.
The selected date is in the input's event.target
*/
console.log("date at input:", date);
};
//2.2 This one stays with the react component and updates when state updates
console.log("date at App:", date);
return (
<div className="App">
<input
//3. The state should be used as value for your input component
value={date}
type="date"
id="birthday"
name="birthday"
onChange={onDateChange}
/>
</div>
);
}
I think you should open anonymous function inside the setDates and type what you want.
Please log the date in console inside a useEffect which gets fired upon change in the dates state like :
useEffect(()=>{
console.log("dates : ", dates);
},[dates])
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;
})
}, []);
I've got a DropDown component (built in a separate component library) which renders a bunch of options.
The dropdown component which I am consuming already supports an array of objects as its source AND i can set the default value quite easily if the list is static - i.e if it does not come from an API.
However, when the options are retrieved via an API call in the consumer application and set via setState I cannot seem to get my default to work.
My goal is to display the regular order of the options if there is no default or display the default if there is one available.
Below is the useEffect hook which aims to do that:
useEffect(() => {
axios
.get(endpoint)
.then(response => {
setDropdownOptions(newObj);
})
.then(() => {
setDefault(relationshipInitialValue);
})
.catch(error => {
// Error handling here
});
}, []);
relationshipInitialValue comes from the props of the component.
What seems to happen however is that the default never gets set and the first option is set as the default.
I am fairly convinced that this is a sync issue but do not know how to proceed. Any help appreciated.
Previous posts seem to focus on class-based components absent of hooks, hence the question.
You need to rerender the dropdown after setting default value in state. You can create a unique key for that dropdown and update that key whenever you want to do like post ajax call.
const [key, setKey] = useState(Date.now());
const [defaultValue, setDefaultValue] = useState('');
useEffect(() => {
// ajax call
// update default Value
}, []);
useEffect(() => {
// update key to rerender Dropdown
setKey(Date.now());
}, [defaultValue]);
<Dropdown key={key} defaultValue={defaultValue} options={options} />
Some time ago I used the default value for the dropdown library for async requests and had similar problems as you. I tried different approaches but in the end, I stopped using the defaultValue and took full control of the dropdown using just currentValue or value depending on the library. It requires slightly more code but you have full control of the dropdown and it's easier to catch bugs with such implementation.
Here's a sample implementation:
const [selectedValue, setSelectedValue] = useState({
label: "Option 1",
value: "option1",
});
return (
<AsyncSelect
getData={getData}
onSelectChange={setSelectedValue}
value={selectedValue}
/>
)
I'm currently learning React and I don't fully understand why this is wrong:
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
And this is correct:
// Correct
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
Could someone give me a real world example where i can use this "second form of setState()" that accepts a function?
This is the link
Say you have a CheckBoxComponent, whose state you initialize like this in the constructor:
this.state = {enabled: true}
You want to update its state when a user clicks on the checkbox. So you write this click handler:
function toggleCheckbox() {
this.setState({enabled: ???});
}
This kind of situation is what the second type of setState is for. The click handler should be written as:
function toggleCheckbox() {
this.setState(prevState => ({enabled: !prevState.enabled}));
}
Because this.props and this.state may be updated asynchronously, you
should not rely on their values for calculating the next state.
1) You can just use this snippet in most situations as long as you didn't use the current state/props to calculate for the next state.
For example, this snippet only goes to fetch data from github and update to its state. We can just put an object inside this.setState().
class FetchGithub extends react.Component{
state = {
//...
};
componentDidMount() {
fetch('https://api.github.com/repos/facebook/react/commits')
.then(data => this.setState(data.json()))
.catch(err => this.setState(err));
}
}
2) But once the scenario is to use the current state/props to calculate for the next state, then you need to put instead a function to make sure that our current state gets updated already.
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
[Updated]
Since props is the argument that is been passed from this component's parent or redux's reducers, which means it takes time to process. So you also need to make sure the props is the most updated one.
Let's look at your code example again. There should be 2 components:
Parent component, that control either +1 or -1 -> AdjustComponent
Child component, just for display the result -> DisplayComponent
So the proper flow is the user click on +1/-1, AdjustComponent passes the props into DisplayComponent. And then DisplayComponent get to update its state by its current state and props sent by AdjustComponent. And show to the screen.
But what if the user click on -1 and then +1 very very very quickly or if user's computer suddenly has huge loading to deal with that affect their browser performance. So that when you use this snippet:
this.setState({
counter: state.counter + props.increment
});
The newest props(which should be +1) hasn't been received from AdjustComponent yet, but DisplayComponent updated already, using the old props.increment, which is -1 that leads to the wrong result.
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.