How to update React setState immediately - reactjs

I'm calling AddItemToList() on "onClick" event.
However, since setTotalExpensesAmount() is asynchronous, it's called too late and axios.post() sends incorrect value of totalExpenses to the database (previous one).
I believe, using useEffect() outside AddItemToList() function is not a correct solution.
Should I make axios.post() the callback of setTotalExpensesAmount()? If so, how can I do it?
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
setExpenseAndAmountList(
[
...expenseAndAmountList,
{
expenseTitle: Expense,
expenseAmount: Amount,
id: Math.random() * 1000
}
]
);
axios.post('http://localhost:4000/app/insertedExpenseAndAmount',
{
expenseTitle: Expense,
expenseAmount: Amount
});
setTotalExpensesAmount((currentTotalExpenses: number) => currentTotalExpenses + Number(Amount));
axios.post('http://localhost:4000/app/totalexpensesamount',
{
totalExpensesAmount: totalExpenses
});
setExpense("");
setAmount("");
setIfNotValidInputs(false);
setTotalBalance(Number(income) - totalExpenses);
} else {
setIfNotValidInputs(true);
}
}

There are 2 ways of doing this.
You can calculate the newTotalExpense and do setTotalExpense with the new value. Also, pass the newTotalExpense in the post call you are making.
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
setExpenseAndAmountList(
[
...expenseAndAmountList,
{
expenseTitle: Expense,
expenseAmount: Amount,
id: Math.random() * 1000
}
]
);
axios.post('http://localhost:4000/app/insertedExpenseAndAmount',
{
expenseTitle: Expense,
expenseAmount: Amount
});
const newTotalExpense = currentTotalExpenses + Number(Amount)
setTotalExpensesAmount(newTotalExpense);
axios.post('http://localhost:4000/app/totalexpensesamount',
{
totalExpensesAmount: newTotalExpense
});
setExpense("");
setAmount("");
setIfNotValidInputs(false);
setTotalBalance(Number(income) - totalExpenses);
} else {
setIfNotValidInputs(true);
}
}
You can use useEffect hook which will trigger on the change of totalExpense value. You can make the post call inside the useEffect.
useEffect(() => {
axios.post('http://localhost:4000/app/totalexpensesamount',
{
totalExpensesAmount: totalExpenses
});
}, [totalExpense])
const AddItemToList = () => {
if (Expense !== '' && Amount !== '' && Number(Amount) > 0) {
setExpenseAndAmountList(
[
...expenseAndAmountList,
{
expenseTitle: Expense,
expenseAmount: Amount,
id: Math.random() * 1000
}
]
);
axios.post('http://localhost:4000/app/insertedExpenseAndAmount',
{
expenseTitle: Expense,
expenseAmount: Amount
});
setTotalExpensesAmount((currentTotalExpenses: number) => currentTotalExpenses + Number(Amount));
setExpense("");
setAmount("");
setIfNotValidInputs(false);
setTotalBalance(Number(income) - totalExpenses);
} else {
setIfNotValidInputs(true);
}
}

Related

this.setState isn't making changes in state

I am using functions that change a value in a nested object in the state :
an I am calling those functions in a button , they are executed when I click on that button , but one of those functions doesn't make changes to the state
This is the state :
state = {
data: {
attributesLength: this.props.product.attributes.length,
modalMessage: "",
isOpen: false,
},
};
and these are the functions :
addToCart = (id) => {
let data = { ...this.state.data };
if (Object.keys(this.state).length === 1) {
data.modalMessage = "Please, select product attributes";
this.setState({ data});
return;
}
if (
Object.keys(this.state).length - 1 ===
this.state.data.attributesLength
) {
const attributes = Object.entries(this.state).filter(
([key, value]) => key !== "data"
);
if (this.props.cartProducts.length === 0) {
this.props.addItem({
id: id,
quantity: 1,
attributes: Object.fromEntries(attributes),
});
data.modalMessage = "Added to cart !";
this.setState({ data });
return;
}
const product = this.props.cartProducts.filter((item) => item.id === id);
if (product.length === 0) {
this.props.addItem({
id: id,
quantity: 1,
attributes: Object.fromEntries(attributes),
});
data.modalMessage = "Added to cart !";
this.setState({ data });
return;
}
if (product.length !== 0) {
this.props.changeQuantity({ id: id, case: "increase" });
data.modalMessage = "Quantity increased !";
this.setState({ data });
return;
}
if (this.state.data.attributesLength === 0) {
this.props.addItem({
id: id,
quantity: 1,
attributes: Object.fromEntries(attributes),
});
data.modalMessage = "Added to cart !";
this.setState({ data });
return;
}
} else {
data.modalMessage = 'please, select "ALL" product attributes!';
this.setState({ data });
}
};
changeModalBoolean = () => {
let data = { ...this.state.data };
data.isOpen = !data.isOpen;
this.setState({ data });
};
and this is where I am calling functions :
<button
className={product.inStock ? null : "disabled"}
disabled={product.inStock ? false : true}
onClick={() => {
this.addToCart(product.id);
this.changeModalBoolean();
}}
>
{product.inStock ? "add to cart" : "out of stock"}
</button>
NOTE
changeModalBoolean function works and change state isOpen value,
this.addToCart(product.id);
this.changeModalBoolean();
This code run synchronously one after the other. In every function, you create a copy of previous state let data = { ...this.state.data };
so the this.changeModalBoolean(); just replace state which you set in this.addToCart(product.id); to fix this problem, use this.setState((state) => /*modify state*/)
changeModalBoolean = () => {
this.setState((state) => {
let data = { ...state.data };
data.isOpen = !data.isOpen;
return { data };
})
};
or modify the same object in both functions

State no updating in class component when testing method with jest

I am trying to test my method in a context provider. I have one branch in the method to be covered and that's what I am strangling with. The specific branch is only entered when a specific condition occurs: if (offset !== 0 && total !== 0 && offset >= total)
See my Class component below:
class JourneyProvider extends Component<
{
children: ReactNode;
},
JourneyContextData
> {
constructor(props: { children: ReactNode }) {
super(props);
this.state = {
...defaultValues,
};
}
getContextValue = (): JourneyContextData => {
const { products, total, limit, offset, loading, disabled, setProducts } =
this.state;
return {
products,
total,
limit,
offset,
loading,
disabled,
setProducts,
};
};
setProducts = async (): Promise<void> => {
const { limit, offset, total, products } = this.state;
if (total === 0 || offset < total) {
const gqlRequest = new GQLRequest(query);
this.setLoading(true);
try {
await gqlRequest.post().then(({ products: { edges, totalCount } }) => {
const newOffset = offset + limit;
this.setState({
products,
total: totalCount,
offset: newOffset,
});
this.setLoading(false);
// Disable button if there are no more products
if (offset !== 0 && total !== 0 && offset >= total) {
// never gets in here when testing.
this.setDisabled(true);
}
});
} catch (e) {
this.setLoading(false);
}
}
};
}
This is my test:
it("setProducts is successful and disable button", async () => {
const wrapper = shallow(
<JourneyProvider>
<div>test</div>
</JourneyProvider>
) as any;
const result = {
products: {
edges: [
{
node: {
id: "1",
name: "test-name",
},
},
],
totalCount: 1,
},
};
mockedClient.post.mockResolvedValueOnce(result);
jest
.spyOn(ProductsQuery, "getProductsQuery")
.mockResolvedValueOnce(new Query("test", true) as never);
const setLoadingSpy = jest.spyOn(wrapper.instance(), "setLoading");
const setDisabledSpy = jest.spyOn(wrapper.instance(), "setDisabled");
wrapper.state().limit = result.products.totalCount;
console.log(wrapper.state().offset); //this returns 0
await wrapper.instance().setProducts();
console.log(wrapper.state().offset); //this returns 0
expect(setLoadingSpy).toHaveBeenCalledWith(true); // this passes
expect(setLoadingSpy).toHaveBeenCalledWith(false); // this passes
expect(setDisabledSpy).toHaveBeenCalledWith(true); // this fails
});
You should change the logic in the code where you're making the comparison because you're using a stale state, keep in mind that a call to setState doesn't change the state immediately. Compare the new values you set to the state instead of the old state values
if (newOffset !== 0 && totalCount !== 0 && newOffset >= totalCount) {
or put that code inside the setState callback to guarantee you're using updated values
...
const newOffset = offset + limit;
this.setState({
products,
total: totalCount,
offset: newOffset,
}, () => {
this.setLoading(false);
if (offset !== 0 && total !== 0 && offset >= total) {
this.setDisabled(true);
}
});
...

How to add some option to a select box above all mapping in React?

I want to add an All Option to my existing select box.
Select box is creating with some API data. With the API data set I want to add an ALL option above.
This is my code.
const useChemicals = () => {
const [data, setData]: any = useState([]);
useEffect(() => {
const getChemicalsData = async () => {
try {
const results = await searchApi.requestChemicalsList();
if (results.data) {
let groupCount = 0;
const chemList: any = [];
results.data.data.chemicals.map((chemical: any, index: number) => {
if (chemical.key === '') {
chemList.push({
label: chemical.value,
options: [],
});
}
});
results.data.data.chemicals.map((chemical: any, index: number) => {
if (chemical.key === '') {
if (index > 1) {
groupCount += 1;
}
} else {
chemList[groupCount].options.push({
label: chemical.value,
value: chemical.key,
});
}
});
setData([...chemList]);
}
} catch (e) {}
};
getChemicalsData();
}, []);
return data && data;
};
export default useChemicals;
How can I add this. Please help me, I am new to React.

Why is not the state updated?

I have a function that updates a state with a change and adds a value, but the state in the 'addResponse' function does not always change:
handleSelected (e, item) {
this.setState({
current_component_id: item.id,
}, () => this.addResponse()
);
};
Call function above:
addResponse (e) {
const { enrollment_id, evaluation_id, user_id, question_id, current_component_id,
responses, current_question, current_question_id
} = this.state;
console.log(current_component_id)
if (current_component_id != 0) {
const newResponse = {
enrollment_id: enrollment_id,
evaluation_id: evaluation_id,
user_id: user_id,
question_id: current_question_id,
answer_component: current_component_id,
};
function hasAnswer(res) {
const list_question_id = res.map((item) => {
return item.question_id
});
if (list_question_id.includes(current_question_id)) {
return true
} else {
return false
}
}
if (responses === undefined) {
this.setState({
responses: [newResponse]
}
, () => console.log('---------> primeiro', this.state.responses)
)
} else {
const check = hasAnswer(responses);
if (check) {
this.setState(prevState => {
prevState.responses.map((item, j) => {
if (item.question_id === current_question_id) {
return item.answer_component = current_component_id
}
return item ;
})
}
, () => { console.log('----> questao alterada ', this.state.responses)}
)
} else {
this.setState({
responses: [...this.state.responses, newResponse]
}
, () => console.log('------> questao nova', this.state.responses)
);
}
}
}
// this.nextQuestion();
};
the first console.log is always correct, but the others do not always change, I know that setState is asyn, but I thought that as I call the addResponse function it would be async
There is a problem in your how you call setState when check is true.
It should be
this.setState(prevState => ({
responses: prevState.responses.map((item, j) => {
if (item.question_id === current_question_id) {
item.answer_component = current_component_id
}
return item ;
})
})
, () => { console.log('----> questao alterada ', this.state.responses)}
)

Mutable version of the filter function to filter results in React?

I have a function called renderExercises which I call in my render function. renderExercises returns an array of ExercisesChoose components.
renderExercises() {
const {selectedType} = this.state;
const allExercises = this.props.exercises;
let exercisesToRender = [];
if (selectedType !== 'all') {
exercisesToRender = allExercises[selectedType];
} else {
exercisesToRender = Object.values(allExercises)
.reduce((array, subarray) => array.concat(subarray), [])
.sort();
}
return exercisesToRender.map((exercise) => {
return (
<ExercisesChoose
key={exercise}
name={exercise}
/>
)
})
}
So far this works. However I also want to filter based on search text if the user has entered this text.
This isn't working as filter can't be called on the existing array exercisesToRender.
if (typeof this.searchText !== 'undefined') {
const searchText = this.searchText.value;
// This is not working
exercisesToRender.filter(item => {
return item.includes(searchText);
});
}
What is the solution to this? Is there a sort method that allows for mutation? If so, is this advisable to use?
This is my current solution which works but is pretty ugly:
renderExercises() {
const {selectedType} = this.state;
const allExercises = this.props.exercises;
let exercisesToRender = [];
if (selectedType !== 'all') {
exercisesToRender = allExercises[selectedType];
} else {
// Combine all the different exercise groups into a single array
exercisesToRender = Object.values(allExercises)
.reduce((array, subarray) => array.concat(subarray), [])
.sort();
}
let render = [];
if (typeof this.searchText !== 'undefined') {
const searchText = this.searchText.value;
render = exercisesToRender.filter(item => {
return item.includes(searchText);
});
} else {
render = exercisesToRender;
}
return render.map((exercise) => {
return (
<ExercisesChoose
key={exercise}
name={exercise}
/>
)
})
}
This is what my exercises object looks like:
this.props.exercises = [
legs:["Squat", "Power squats", "Burpees"]
pull:["Pull up", "Chin up", "Dumbbell curl", "Horizontal row"]
push:["Push up", "Bench press", "Dumbbell bench press", "Mountain climbers"]
cardio: ["Running high knees", "Plank", "Crunches", "Skipping"]
]
My strategy for this case would be:
reduce to filter exercises by type
filter them by searchText
sort
map to render
Final result:
renderExercises() {
const { selectedType } = this.state
const { exercises: allExercises } = this.props
return Object
.keys(allExercises)
.reduce((result, key) => {
if (selectedType === 'all' || key === selectedType) {
return [
...result,
...allExercises[key],
]
}
return result
}, [])
.filter(exercise => searchText ? exercise.includes(searchText) : true)
.sort()
.map(exercise =>
<ExercisesChoose
key={exercise}
name={exercise}
/>
)
}
const exercises = {
legs:["Squat", "Power squats", "Burpees"],
pull:["Pull up", "Chin up", "Dumbbell curl", "Horizontal row"],
push:["Push up", "Bench press", "Dumbbell bench press", "Mountain climbers"],
cardio: ["Running high knees", "Plank", "Crunches", "Skipping"],
}
const filterExercises = (type, searchText) => {
return Object
.keys(exercises)
.reduce((result, key) => {
if (type === 'all' || key === type) {
return [
...result,
...exercises[key],
]
}
return result
}, [])
.filter(exercise => searchText ? exercise.includes(searchText) : true)
.sort()
.join(', ')
}
console.log('All exercises:', filterExercises('all', ''))
console.log('All (up):', filterExercises('all', 'up'))
console.log('Push:', filterExercises('push', ''))
console.log('Push (press):', filterExercises('push', 'press'))
Ive expanded slightly on mersocarlin's answer as I was getting some false results from searchText, but essentially his logic does work.
renderExercises() {
const {selectedType} = this.state;
const allExercises = this.props.exercises;
let searchText = false;
if (this.searchText && this.searchText.value.length > 0) {
searchText = this.searchText.value.toLowerCase();
}
return Object
.keys(allExercises)
.reduce((result, key) => {
if (selectedType === 'all' || key === selectedType) {
return [
...result,
...allExercises[key]
]
}
return result
}, [])
.filter(exercise => searchText ? exercise.toLowerCase().includes(searchText) : true)
.map((exercise) => {
let active = false;
if (this.props.chosenExercise === exercise) {
active = true;
}
return (
<ExercisesChoose
key={exercise}
name={exercise}
active={active}
setNumber={this.props.number}
updateValue={this.props.updateValue}
/>
)
})
}

Resources