I made a component that load data via xhr on the user select a value of <select> element.
class SomeComponent extends Component {
state = {
data: [],
currentCategory: 'all'
}
switchCategory = (ev) => {
console.log('Selected category is ' + ev.target.value);
this.setState({
currentCategory: ev.target.value
});
this.loadData();
}
loadData = async () => {
let { currentCategory } = this.state;
// Always print previous value!!!
console.log(currentCategory);
// Get data via XHR...
}
render() {
return (
<div>
<select value={currentCategory} onChange={this.switchCategory}>
<option value="all">All</option>
{categories.map( category =>
<option key={category._id} value={category.category}>{category.display}</option>
)}
</select>
<table>
// ... prints data with this.state.data
</table>
</div>
);
}
}
Above code is just in brief. Code is quite simple, I just synchronize a value of the select element with this.state.currentCategory, and detect it's switching with switchCategory method of the class.
But the main problem is that when I access the component's state, it always contains previous value, not a present. You can see that I updating the currentCategory on value of the select changes.
switchCategory = (ev) => {
console.log('Selected category is ' + ev.target.value);
this.setState({
currentCategory: ev.target.value
});
this.loadData();
}
So in this situation, this.state.currentCategory must not has "all", like something else "Apple", but still it contains "all", not an "Apple"!
loadData = async () => {
let { currentCategory } = this.state;
// Always print previous value!!! I expected "Apple", but it has "all"
console.log(currentCategory);
// Get data via XHR...
}
So eventually XHR occurs with previous value, and it gives me wrong data that I didn't expected.
After that, choosing other value of the select(let's call it Banana), it has an Apple, not a Banana!
As I know setState is "sync" job, so calling this.switchCategory will happens after updating states, so it must have present value, not a previous.
But when I print the component's state in console, it isn't.
So, what am I missing? Why am I always getting old data, not present? If I doing something wrong approach, then what alternatives can I do?
Any advice will very appreciate it. Thanks!
The problem here is that setState is async (it can be sync in certain situations). That's why you are get previous value.
There are two possible solutions.
//
// 1. use value directly.
//
switchCategory = (ev) => {
this.setState({ currentCategory: ev.target.value });
this.loadData(ev.target.value);
}
loadData = async (currentCategory) => {
console.log(currentCategory);
// Get data via XHR...
}
//
// 2. use completition callback on `setState`.
//
switchCategory = (ev) => {
this.setState({ currentCategory: ev.target.value }, () => {
this.loadData(ev.target.value);
});
}
loadData = async () => {
const { currentCategory } = this.state;
console.log(currentCategory);
// Get data via XHR...
}
Article on synchronous setState in React [link]
Related
So I'm trying to render out my data and I want to have a filter so I could filter through them. My filter looks something like this:
So far, I have logic that is being used to filter completed and deleted item and it is connected to the <Switch /> (toggle) as shown in the picture. The code is as follows:
Note that items is the original array of items and filteredItems is what I mutate everytime I filter.
useEffect(() => {
if (completedSwitch && deletedSwitch) {
setFilteredItems(
items
.filter((el) => el.completed && el.deleted)
);
} else if (completedSwitch) {
setFilteredItems(
items
.filter((el) => el.completed)
);
} else if (deletedSwitch) {
setFilteredItems(
items
.filter((el) => el.deleted)
);
} else {
setFilteredItems(items);
}
// eslint-disable-next-line
}, [completedSwitch, deletedSwitch]);
What I do not understand is how can I integrate the rest of the filters (the multi-select from username, type, variety, size to the useEffect logic. I know I shouldn't be writing all of the conditions into an if/else statement which would be a little crazy. How should I approach this problem? Separate them into 2 useEffect? What is your take on this problem?
Additional info:
Currently, I'm storing all of the multi-select values into a state, so I have something like:
const [filteredUser, setFilteredUser] = useState<string>("");
// and so on...
// and an event handler to store the data
const handleUserSelect = (e: React.FormEvent<HTMLSelectElement>) => {
setFilteredUser(e.currentTarget.value);
}
// the multi-select component
<HTMLSelect onChange={handleUserSelect}>
<option>User</option>
<option value="A">A</option>
<option value="B">B</option>
<option value="C">C</option>
// and so on...
</HTMLSelect>
I would suggest you to handle this in different way. like store selected filters and data in redux or shared state and
using selector you can provide your on custom code which will filter out the data based on your selected filters.
export const selectFiltered = (state: IApplicationState): IData[] => {
let data: IData[] = [...state.data];
data = filterBySearchText(data, state.filter.searchText);
return data;
};
const filterdData = useSelector((state: IApplicationState) => selectFiltered(state));
Maybe the filter condition could be expressed like this:
useEffect(() => {
setFilteredItems(
items.filter((el) => {
if(completedSwitch !== el.completed) return false;
else if(deletedSwitch !== el.deleted) return false;
else if(filteredUser && el.username !== filteredUser) return false;
// ... the other filter conditions for your multi select values
else return true;
}
)
// eslint-disable-next-line
}, [completedSwitch, deletedSwitch, filteredUser, /*...*/]);
Below both code does exactly same but in different way. There is an onChange event listener on an input component. In first approach I am shallow cloning the items from state then doing changes over it and once changes are done I am updating the items with clonedItems with changed property.
In second approach I didn't cloned and simply did changes on state items and then updated the state accordingly. Since directly (without setState) changing property of state doesn't call updating lifecycles in react, I feel second way is better as I am saving some overhead on cloning.
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const clonedItems = Array.from(items);
clonedItems.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items: clonedItems });
};
OR
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
ele.rate = Number(value);
}
});
this.setState({ items });
};
You can use this
this.setState(state => {
const list = state.list.map(item => item + 1);
return {
list,
};
});
if you need more info about using arrays on states, please read this: How to manage React State with Arrays
Modifying the input is generally a bad practice, however cloning in the first example is a bit of an overkill. You don't really need to clone the array to achieve immutability, how about something like that:
handleRateChange = (evnt: React.ChangeEvent<HTMLInputElement>) => {
const {
dataset: { type },
value,
} = evnt.target;
const { items } = this.state;
const processedItems = items.map((ele: NetworkItem) => {
if (ele.nicType === type) {
return {
...ele,
rate: Number(value)
};
} else {
return ele;
}
});
this.setState({ items: processedItems });
};
It can be refactored of course, I left it like this to better illustrate the idea. Which is, instead of cloning the items before mapping, or modifying its content, you can return a new object from the map's callback and assign the result to a new variable.
I have modal component with form. I want to inform fields of this form that form data was successfully sent to database and clear its fields.
Component code:
//ItemModal.js
addItem(e) {
e.preventDefault();
const item = {
id: this.props.itemsStore.length + 1,
image: this.fileInput.files[0] || 'http://via.placeholder.com/350x150',
tags: this.tagInput.value,
place: this.placeInput.value,
details: this.detailsInput.value
}
console.log('addded', item);
this.props.onAddItem(item);
this.fileInput.value = '';
this.tagInput.value = '';
this.placeInput.value = '';
this.detailsInput.value = '';
this.setState({
filled: {
...this.state.filled,
place: false,
tags: false
},
loadingText: 'Loading...'
});
}
...
render() {
return (
<div className="text-center" >
<div className={"text-center form-notification " + ((this.state.loadingText) ? 'form-notification__active' : '' )}>
{(this.state.loadingText) ? ((this.props.loadingState === true) ? 'Item added' : this.state.loadingText) : '' }
</div>
)
}
action.js
export function onAddItem(item) {
axios.post('http://localhost:3001/api/items/', item )
.then(res => {
dispatch({type:"ADD_ITEM", item});
dispatch({type:"ITEM_LOADED", status: true});
})
}
helper.js
else if (action.type === 'ITEM_LOADED') {
const status = action.status;
return {
...state,
isItemLoaded: status
}
}
Currently I have few issues with my code:
1. field are clearing right after click, but they should clear after changing state of loadingState. I tried to check it in separate function on in componentWillReceiveProps whether state is changed and it worked, but I faces another problem, that after closing this modal there were errors, that such fields doesn't exist.
2. loadingText should become '' (empty) after few seconds. Tried same approach with separate function and componentWillReceiveProps as at first issue.
In constructor keep a copy of your initial state in a const as follows:
const stateCopy = Object.create(this.state);
When your ajax request completes, in the sucess callback you can reset the state with this copy as follows:
this.setStae({
...stateCopy
});
One of the few ways to achieve this is to use async await which will resolve the promises and then return the value after that you can clear the values
1st approach using the async await
Here is the example
handleSubmit = async event => {
event.preventDefault();
// Promise is resolved and value is inside of the response const.
const response = await API.delete(`users/${this.state.id}`);
//dispatch your reducers
};
Now in your react component call it
PostData() {
const res = await handleSubmit();
//empty your model and values
}
Second approach is to use the timer to check the value is changed or not
for this we need one variable add this to the service
let timerFinished=false;
one function to check it is changed or not
CheckTimers = () => {
setTimeout(() => {
if (timerFinished) {
//empty your modal and clear the values
} else {
this.CheckTimers();
}
}, 200);
}
on your add item change this variable value
export function onAddItem(item) {
axios.post('http://localhost:3001/api/items/', item)
.then(res => {
timerFinished = true;
dispatch({
type: "ADD_ITEM",
item
});
dispatch({
type: "ITEM_LOADED",
status: true
});
})
}
and here is how we need to call it.
PostData = (items) => {
timerFinished = false;
onAddItem(items);
this.CheckTimers();
}
If you check this what we done is continuously checking the variable change and emptied only once its done.
One thing you need to handle is to when axios failed to post the data you need to change the variable value to something and handle it, you can do it using the different values 'error','failed','success' to the timerFinished variable.
I am trying to change store on (addList) event and check if this is the first list an array of lists to fire (selectList) event and add it to (state.selectedList.list). Now (selectList) is used whenever onClick event is working on an array of lists.
The question is when and how should I handle the event of adding first added a list to selectedList and than use (selectList) only onClick event as I used before.
export default connect(
state => ({
lists: getEntities('lists')(state),
selectedList: state.selectedList.list
}),
dispatch => ({
addList: (name) => dispatch({type: 'ADD_LIST', payload: name}),
selectList: (listId) => dispatch({type: 'CHANGE_SELECTED_LIST', payload: listId})
})
)(Lists)
If you have both of name and id of list you adding by click, then it could be done right in the addList handler:
addList(name, id) { // bound with click
const size = this.props.lists.length
this.props.addList(name)
if(!size) {
this.props.selectList(id)
}
}
Otherwise, you need to wait for list is created (to get the id) and then dispatch selectList action. The dirtiest solution could be just:
addList(name) { // bound with click
const size = this.props.lists.length
this.props.addList(name)
if(!size) {
// don't do that! it's just for proof of concept
setTimeout(() => this.props.selectList(this.props.lists[size].id))
}
}
Going further, you may use componentWillReceiveProps hook to catch the lists.length becames 1 and then trigger selectList normally:
componentWillReceiveProps(nextProps) {
const previousSize = this.props.lists.length
const size = nextProps.lists.length
if(previousSize === 0 && size === 1) {
this.props.selectList(nextProps.lists[size].id))
}
}
Another option would be a reducer where we don't deal with new list id, but only with a previous lists size (and no need for additional action trigger):
case ADD_LIST:
return {...state,
lists: [...state.lists,
action.newList
],
selectList: {...state.selectList,
list: !state.lists.length ? action.newList : state.selectList.list
}
}
At last, instead of all above, the action creator (the favorite one for me):
export function addList(name) {
return (dispatch, getState) => {
const size = getState().lists.length
const newList = generateNewListByName(name)
dispatch({
type: ADD_LIST,
newList
})
if(size === 0)
dispatch({
type: CHANGE_SELECTED_LIST,
selectedListId: newList.id
})
}
}
}
The last pice needs to be adapted to your infrastructure, but the main idea remains the same: do some logic by some previous state condition (size === 0).
I am trying to pass my parent App state to a child component Chart.
App
constructor() {
super();
this.state = {
dataPoints: {
'424353': {
date: '10/10/2016',
qty: '95'
},
'535332': {
date: '10/11/2016',
qty: '98'
},
'3453432': {
date: '10/01/2017',
qty: '94'
}
}
};
this.addPoint = this.addPoint.bind(this);
}
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
render() {
return (
<div className="app">
<Chart dataPoints={this.state.dataPoints} />
<FormControl addPoint={this.addPoint} />
</div>
);
}
Chart
composeData() {
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
componentWillUpdate() {
this.composeData();
}
The addPoint method works, i.e. I can see in the React console that the new datapoint is added to the state. But it is not reflected in the Chart component. More oddly (to me) is the fact that when I've added a point, the console.log line in my addPoint method (above):
console.log('new state', this.state.dataPoints)
does not show the new data point.
In add point
addPoint(dataPoint) {
let dataPoints = {...this.state.dataPoints};
const timestamp = Date.now();
dataPoints[timestamp] = dataPoint;
this.setState({ dataPoints: dataPoints });
console.log('new state', this.state.dataPoints);
}
In the above code you do not see the updated value because setState takes time to mutate, You must log it in the setState call back
this.setState({ dataPoints: dataPoints }, function(){
console.log('new state', this.state.dataPoints);
});
And also in the Chart component you need to bind the composeData function if you are using this.props inside it like
composeData = () =>{
Object
.keys(this.props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
However componentWillMount is only called once and hence you will want to call the composeData function from componentWillReceiveProps also like
componentWillReceiveProps(nextProps) {
this.composeData(nextProps)
}
componentWillMount() {
this.composeData(this.props.dataPoints)
}
composeData(props){
Object
.keys(props.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Because setState is asynchronous, use this you see the updated values:
this.setState({ dataPoints: dataPoints }, () => {
console.log('new state', this.state.dataPoints);
});
Second thing is, whenever any change happen to props values, componentWillReceiveProps lifecycle method will get called, do the computation in this method once you add the new item. Like this:
componentWillReceiveProps(newProps) {
console.log('update props values', newProps);
Object
.keys(newProps.dataPoints)
.forEach(key => {
** do stuff **
});
return **stuff**;
}
Check this answer for complete explanation: why setState is asynchronous.