prevent api from being called before state is updated - reactjs

I have a list of objects. I want to make an api call once the location field of the object is changed. So for that, I have a useEffect that has id, index and location as its dependencies. I have set a null check for the location, if the location isn't empty, I want to make the api call. But the thing is, the api is being called even when the location is empty, and I end up getting a 400. How can I fix this and make the call once location isn't empty?
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
coords: {},
country: '',
isOffshore: false,
}
]);
const [locationIDObject, setlocationIDObject] = useState({
id: plants[0].id,
index: 0
});
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
setlocationIDObject({
id: id,
index: index
})
}
const getCoords = (id, index) => {
axios.get('http://localhost:3002/api/coords', {
params: {
locAddress: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 5000), []);
useDeepCompareEffect(() => {
if(plants[locationIDObject.index].location !== '')
handler(locationIDObject.id, locationIDObject.index);
}, [ locationIDObject, plants[locationIDObject.index].location])
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)

I think your useCallback is not updating when value of your variables is changing and that is the issue:
Although the check is correct, but the call is made for older values of the variable. You should update the dependencies of your useCallback:
console.log(plants) inside getCoords might help you investigate.
Try this:
const handler = useCallback(debounce(getCoords, 5000), [plants]);

So it turns out the issue was with my debouncing function. I don't know what exactly the issue was, but everything worked as expected when I replaced the debouncing function with this:
useEffect(() => {
console.log("it changing")
const delayDebounceFn = setTimeout(() => {
getCoords(locationIDObject.id, locationIDObject.index)
}, 4000)
return () => clearTimeout(delayDebounceFn)
},[...plants.map(item => item.location), locationIDObject.id, locationIDObject.index])

Related

useCallback not updating array with object

I am trying to update array with objects using useCallback but instead of updating object it is adding or appending one too.
const items = [
{
id: 101,
name: 'Mae Jemison',
},
{
id: 201,
name: 'Ellen Ochoa',
},
];
const [headings, setHeadings] = useState(items);
const handleHeadingTextChange = useCallback(
(value, id) => {
let items2 = headings;
items2 = items2.map((item, key) => {
if (items2[key].id == id) {
items2[key].name = value
}
return items2
});
setHeadings((prevState) => [...prevState, items2]) // adding, not updating
//setHeadings((prevState) => [...prevState, ...items2]) // try 2 : still adding
},
[setHeadings],
);
<input type="text" id="101" value="" onChange={handleHeadingTextChange} />
So, on change expected output is
items = [
{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
},
];
But Instead I am getting
items = [
{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
},
[{
id: 101,
name: 'Johaan',
},
{
id: 201,
name: 'Ellen Ochoa',
}]
];
I am not sure how to set value in setHeading function so that it only update the value and not appending one too. Is there way to make it update only?
problems
.map iterates over the entire array. that doesn't make sense if you are trying to just update a single item.
onChange takes an event handler, but your handler accepts (value, id)
the value property of your <input> should be set from the object's name property
solution
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = key => event => {
setHeadings(prevState => {
return [
// elements before the key to update
...prevState.slice(0, key),
// the element to update
{ ...prevState[key], name: event.currentTarget.value },
// elements after the key
...prevState.slice(key + 1),
]
})
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
)
}
improvement
Imagine having to write that function each time you have a component with array or object state. Extract it to a generic function and use it where necessary -
// generic functions
function arrUpdate(arr, key, func) {
return [...arr.slice(0, key), func(arr[key]), ...arr.slice(key + 1)]
}
function objUpdate(obj, key, func) {
return {...obj, [key]: func(obj[key])}
}
// simplified component
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = key => event => {
setHeadings(prevState =>
// update array at key
updateArr(prevState, key, elem =>
// update elem's name property
updateObj(elem, "name", prevName =>
// new element name
event.currentTarget.value
)
)
)
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={updateName(key)} />
)
}
multiple arrow functions?
Curried functions make writing your event handlers easier, but maybe you've never used them before. Here's what the uncurried version would look like -
function MyComponent() {
const [headings, setHeadings] = useState(items);
const updateName = (key, event) => {
setHeadings(prevState =>
// update array at key
updateArr(prevState, key, elem =>
// update elem name property
updateObj(elem, "name", prevName =>
// new element name
event.currentTarget.value
)
)
)
}
return headings.map((h, key) =>
<input key={key} id={h.id} value={h.name} onChange={e => updateName(key, e)} />
)
}

Patch multiple id's with one request with React Query

I have a very basic prototype of app that allows to book a seat. User selects the seat/seats, clicks book, patch request with available: false is sent to the fake api (json-server) with React Query, library invalidates the request and immediately shows response from the server.
Database structure looks like this:
{
"hallA": [
{
"id": 1,
"seat": 1,
"available": false
},
{
"id": 2,
"seat": 2,
"available": true
},
{
"id": 3,
"seat": 3,
"available": false
}
]
}
and the logic for selecting, booking seats looks like this:
const App = () => {
const { data, isLoading } = useGetHallLayout("hallA");
const [selected, setSelected] = useState<
{ id: number; seat: number; available: boolean }[]
>([]);
const handleSelect = useCallback(
(seat: { id: number; seat: number; available: boolean }) => {
const itemIdx = selected.findIndex((element) => element.id === seat.id);
if (itemIdx === -1) {
setSelected((prevState) => [
...prevState,
{ id: seat.id, seat: seat.seat, available: !seat.available },
]);
} else {
setSelected((prevState) =>
prevState.filter((element) => element.id !== seat.id)
);
}
},
[selected]
);
const takeSeat = useTakeSeat({
onSuccess: () => {
useGetHallLayout.invalidate();
},
});
const sendRequest = useCallback(() => {
selected.forEach((item) =>
takeSeat.mutateAsync({ id: item.id, hall: "hallA" })
);
setSelected([]);
}, [selected, takeSeat]);
return (
<>
{!isLoading && (
<ConcertHall
layout={data}
onSeatSelect={handleSelect}
activeSeats={selected}
/>
)}
<button disabled={isLoading} onClick={sendRequest}>
Take selected
</button>
</>
);
};
Queries look like this:
export const useGetHallLayout = (hall: string) => {
const { data, isLoading } = useQuery(["hall"], () =>
axios.get(`http://localhost:3000/${hall}`).then((res) => res.data)
);
return { data, isLoading };
};
export const useTakeSeat = (options?: UseMutationOptions<unknown, any, any>) =>
useMutation(
(data: { hall: string; id: number }) =>
axios.patch(`http://localhost:3000/${data.hall}/${data.id}`, {
available: false,
}),
{
...options,
}
);
useGetHallLayout.invalidate = () => {
return queryClient.invalidateQueries("hall");
};
The problem of the above code is that I perform very expensive operation of updating each id in a for each loop (to available: false) and query invalidates it after each change not once all of them are updated.
The question is: is there any better way to do this taking into account the limitations of json-server? Any batch update instead of sending request to each and every id seperately? Maybe some changes in a logic?
Thanks in advance
You can certainly make one mutation that fires of multiple requests, and returns the result with Promise.all or Promise.allSettled. Something like:
useMutation((seats) => {
return Promise.allSettled(seats.map((seat) => axios.patch(...))
})
then, you would have one "lifecycle" (loading / error / success) for all queries together, and onSuccess will only be called once.
Another gotcha I'm seeing is that you'd really want the hall string to be part of the query key:
- useQuery(["hall"], () =>
+ useQuery(["hall", hall], () =>

run function once a value in an object has been updated

I have a list of objects that have multiple fields. I want to make an api call once the user has entered the location of the plant.
I thought of using useEffect, but that won't work since in order to make the api call, I need two values - id and index, and (as far as I know) you can't pass parameters in useEffect. I did it normally (which you can see in the code below), instead of using useEffect, but that doesn't work because the plant location is empty (as there is a bit of a delay before the plant location is updated).
So, how can I make the call once the plant location has been updated?
//the plants object
const [plants, setPlants] = useState([
{
name: 'Plant 1',
id: uuidv4(),
location: '',
isOffshore: false,
coords: {},
country: '',
}
])
const handlePlantLocChange = (id, index, value) => {
setPlants(
plants.map(plant =>
plant.id === id
? {...plant, location: value}
: plant
)
)
handler(id, index);
}
// this makes the api call.
//I need to pass id and index to this function
//no matter what, hence why I can't use useEffect
const getCoords = (id, index) => {
axios.get('http://localhost:3001/endpoints/coords', {
params: {
location: plants[index].location
}
}).then((response) => {
if(response.status === 200) {
handlePlantInfoChange(id, PlantInfo.COORD, response.data)
}
})
}
const handler = useCallback(debounce(getCoords, 4000), []);
return (
<div className='plant-inps-wrapper'>
{
plants.map((item, idx) => {
return (
<div key={item.id} className="org-input-wrapper">
<input placeholder={`${item.isOffshore ? 'Offshore' : 'Plant ' + (idx+1) + ' location'}`} onChange={(e) => handlePlantLocChange(item.id, idx, e.target.value)} value={item.isOffshore ? 'Offshore' : item.location} disabled={item.isOffshore} className="org-input smaller-input"/>
</div>
)
})
}
</div>
)
you can pass props and state to useEffect like this:
useEffect(() => {
getCoords(id, index);
return () => getCoords(id, index);
}, [id, index])
Please use useEffect hook with dependency array.
Like below:
useEffect(() => {
// You can call API here because When plant state updated this will be called.
}, [plants]);

TypeError: inputs.lineItems is undefined React

const [inputs, setInputs] = useState({
taxRate: 0.00,
lineItems: [
{
id: 'initial',
name: '',
description: '',
quantity: 0,
price: 0.00,
},
]
});
function handleInvoiceChange(e) {
//setInputs(inputs => ({...inputs,[e.target.name]: e.target.value}));
setInputs({[e.target.name]: e.target.value});
}
const calcLineItemsTotal = (event) => {
return inputs.lineItems.reduce((prev, cur) => (prev + (cur.quantity * cur.price)), 0)
}
const calcTaxTotal = () => {
return calcLineItemsTotal() + (inputs.taxRate / 100)
}
and this is how i handle the change
const handleLineItemChange = (elementIndex) => (event) => {
let lineItems = inputs.lineItems.map((item, i) => {
if (elementIndex !== i) return item
return {...item, [event.target.name]: event.target.value}
})
setInputs(inputs => ({...inputs,[lineItems]:lineItems}));
//setInputs({lineItems})
}
const handleAddLineItem = (event) => {
setInputs({
lineItems: inputs.lineItems.concat(
[{ id: uuidv4(), name: '', description: '', quantity: 0, price: 0.00 }]
)
})
}
const handleRemoveLineItem = (elementIndex) => (event) => {
setInputs({
lineItems: inputs.lineItems.filter((item, i) => {
return elementIndex !== i
})
})
}
this is a react application of an invoice generator the problem occures when i add the taxrate then i get that error
Updated values to states with hooks are not merged but replaced.
Also if you are using a version of v16 or lower of react know that Synthetic event is pooled by react, i.e event object is cleared before state callback runs.
Check here for more information.
The SyntheticEvent objects are pooled. This means that the
SyntheticEvent object will be reused and all properties will be
nullified after the event handler has been called.
The correct way to update your state is as below where you use function way to update state and copy the event values you need to separate variables outside of the setInputs function call
const name = e.target.name;
const value = e.target.value;
setInputs(inputs => ({...inputs,[name]: value}));
The rest of your function updates will be as below
const handleAddLineItem = (event) => {
setInputs(input => ({
...input,
lineItems: inputs.lineItems.concat(
[{ id: uuidv4(), name: '', description: '', quantity: 0, price: 0.00 }]
)
}));
}
const handleRemoveLineItem = (elementIndex) => (event) => {
setInputs(input =>({
...input,
lineItems: inputs.lineItems.filter((item, i) => {
return elementIndex !== i
})
}));
}

Delete one item in an array ReactJS (React Hooks and Redux)

I'm creating a recipe application using MERN stack.
The issue I am stuck on is trying to delete an ingredient found in an array, inside a recipe object. My recipe object looks like this:
MongoDB Recipe Object
Each ingredient has a cross next to it that allows you to click on it to remove.
<div>
<ol className='pad5px20px'>
{addIngredients.ingredients.map(data => (
<div className='ingredient-item padTB5px' key={data}>
<li>{data}</li>
<span onClick={() => removeIngredient(data)}>
<i className='fas fa-trash'></i>
</span>{' '}
      
</div>
))}
</ol>
</div>
The addIngredients and removeIngredient functions look like this:
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
addIngredients.ingredients.push(query);
setIngredients(addIngredients);
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
setIngredients(
addIngredients.ingredients.filter(e => e.ingredients !== data)
);
};
Every time I remove an ingredient from my list I get an error that states "TypeError: Cannot read property 'map' of undefined".
Is there something that i'm missing here? I have been working on this app for the past couple of months now and I am stuck on this particular bit. I thought a better way would be to use Redux as I have been able to delete a whole recipe using a reducer:
case DELETE_RECIPE:
return {
...state,
recipes: state.recipes.filter(recipe => recipe._id !== action.payload),
loading: false
};
but how would I be able to target one particular ingredient?
Any suggestions would be greatly appreciated.
I added notes to your code problems)
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
***STATE MUTATION***
addIngredients.ingredients.push(query);
setIngredients(addIngredients);
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
***You're adding ingredients object instead of addIngredients as you used in addIngredient method***
setIngredients(
addIngredients.ingredients.filter(e => e.ingredients !== data)
);
};
addIngredients.ingredients.filter(e => e.ingredients !== data) returns filtered ingredients instead of addIngredients with filtered ingredients field
How it should be
const addIngredient = e => {
e.preventDefault();
if (query === '') return;
setIngredients({
...addIngredients,
ingredients: [
...addIngredients,
query
]
});
setRecipe(prevState => ({
...prevState,
ingredients: [
...prevState.ingredients,
{ id: Date.now(), ingredient: query }
]
}));
};
const removeIngredient = data => {
const results = addIngredients.ingredients.filter(
e => e.ingredients !== data
);
setIngredients(
{
...addIngredients,
ingredients: results
});
};

Resources