ReactJS component won't update after first API call - javascript

So I recently started to discover ReactJS. I have a simple Spring Boot api which has a few methods. One of these returns a list of objects. I get these in my frontend by using Axios to make the HTTP call.
export function getItems() {
const [items, setItems] = useState([]);
useEffect(async () => {
await client.get('items').then((result) => {
setItems(result.data);
});
}, []);
return items;
The items are mapped in a gallery component and shown on screen. Next to this, I have an addItem function which posts an item object obtained through a form to my api.
export async function addPet(newPet) {
await client.post(
'/pets',
newPet,
);
}
My AddPet component is loaded inside my Gallery component. The form is shown on the right side of the screen and when I click the "Add Item" button, I want the item to be added and my list of items reloaded to show the newly added item. Right now, I can not get this working in a correct way. If I remove the "[]" from the final part of my useEffect() in my getItems() functions, everything seems to work but in reality the app is making the getItems call over and over again. If I add "[]", the call is only made once at the start, but will not re-render the gallery component when an item is added. The handleSubmit() for my "Add item" button is as follows:
const handleSubmit = () => {
const newItem = new Item();
newItem .name = formValue.name;
newItem .image = formValue.image;
newItem .pitemText = formValue.itemText;
addItem(newItem);
};
So my question here is: how can I get that gallery component to re-render whenever I add a new item or delete one of the items? I figure I need a way to change the state of the component but I can't seem to get it done.

The second parameter of useEffect (the Array) has an important role: the items in that array trigger the useEffect to re-run.
Some cases:
useEffect(() => {}, []): runs once, after the component is mounted
useEffect(() => {}, [var1, var2,..., varn]): runs when var1 or var2 or varn is updated
useEffect(() => {}): runs on every completed re-render (default behavior)
More on useEffect: useEffect hook
So, your code works as expected:
useEffect(() => {
client.get('items').then((result) => {
setItems(result.data);
});
}, []); // -> runs once, when component is mounted
useEffect(() => {
client.get('items').then((result) => {
setItems(result.data);
});
}, [item]); // -> runs when the variable named item changes
you need to organize your code in such a way, that this useEffect hook can run on the update of the variable whose change you want to watch.

dont pust async as the first parameter of useEffect hook as below, wont work well
useEffect(async () => {
await client.get('items').then((result) => {
setItems(result.data);
});
}, []);
instead you can use external function or IIEF function as below
useEffect(() => {
(async () => {
await client.get('items').then((result) => {
setItems(result.data);
});
})()
}, []);

Related

using state from a context provider within my useEffect dependency array causes an infinite loop

I am using the shopify-buy SDK, which allows me to retrieve the current cart of the user. I am trying to store that cart in my CartProvider which is then used in my Cart component. The problem is when I retrieve information from the cart it's acting a little slow so my component needs to be updated when the state changes, currently I have the following in my getShopifyCart function which is located in my CartProvider.
const [cartItems, setCartItems] = useState([])
const getShopifyCart = () => {
return client.checkout
.fetch(currentVendor.cartId)
.then((res) => {
const lineItemsData = res.lineItems.map((item) => {
return {
title: item.title,
quantity: item.quantity,
}
})
setCartItems(lineItemsData)
setLoading(false)
})
.catch((err) => console.log(err))
}
In my Cart component I have the following useEffect.
useEffect(() => {
getShopifyCart()
}, [cartItems])
But this causes an infinite loop, even though the cartItems state isn't changing.
You are setting the state cartItems inside getShopifyCart which you are calling inside a useEffect which has cartItems as a dependency. Even though the content of the data has not changed, you are creating a new object, hence its hash has changed as well which causes the useEffect to be called again.
If you want to initially fetch the data and set the state, then you need to pass an empty dependency array.
useEffect(() => {
getShopifyCart()
}, [])

React Component not rendering a passed props

I'm trying to pass an array of users as a props to a component, when I change something and click save, the array is showed in the component but when I hit refresh, the array is disappeared, here is my code:
First in my App.js I'm reading an array of users from the database (works perfectly shows the list of users) :
const [users,setUsers] = React.useState([]);
React.useEffect( () => {
async function fetchData() {
await axios.get('/api/users/')
.then(response => {
setUsers(response.data);
});
}
fetchData();
}
, []);
Then, also in App.js, I'm rendering a ListComponent that takes the users array and shows the users:
return (
<ListComponent users={users} />
);
}
In my ListComponent after a page refresh the console.log shows an empty array []
const ListComponent = (props) => {
React.useEffect(()=>{
console.log(props.users); // []
},[])
When you refresh the page, the ListComponent will be remounted, and what you are logging, is the state of the component just after it is mounted, so the user array is not already fetched. If you want log it when it is fetched, you should add the user array in the dependency array of the useEffect function:
const ListComponent = (props) => {
React.useEffect(()=>{
console.log(props.users); // Should be an empty array first, then updated if your fetch function is working properly
},[props.users]);
// ...
};
If you still cannot see the user array, it means that something is not happening as expected in your fetchData function I guess.

How to prevent state updates from a function, running an async call

so i have a bit of a weird problem i dont know how to solve.
In my code i have a custom hook with a bunch of functionality for a fetching a list
of train journeys. I have some useEffects to that keeps loading in new journeys untill the last journey of the day.
When i change route, while it is still loading in new journeys. I get the "changes to unmounted component" React error.
I understand that i get this error because the component is doing an async fetch that finishes after i've gone to a new page.
The problem i can't figure out is HOW do i prevent it from doing that? the "unmounted" error always occur on one of the 4 lines listed in the code snippet.
Mock of the code:
const [loading, setLoading] = useState(true);
const [journeys, setJourneys] = useState([]);
const [hasLaterDepartures, setHasLaterDepartures] = useState(true);
const getJourneys = async (date, journeys) => {
setLoading(true);
setHasLaterDepartures(true);
const selectedDateJourneys = await fetchJourney(date); // Fetch that returns 0-3 journeys
if (condition1) setHasLaterDepartures(false); // trying to update unmounted component
if (condition2) {
if (condition3) {
setJourneys(something1); // trying to update unmounted component
} else {
setJourneys(something2) // trying to update unmounted component
}
} else {
setJourneys(something3); // trying to update unmounted component
}
};
// useEffects for continous loading of journeys.
useEffect(() => {
if (!hasLaterDepartures) setLoading(false);
}, [hasLaterDepartures]);
useEffect(() => {
if (hasLaterDepartures && journeys.length > 0) {
const latestStart = ... // just a date
if (latestStart.addMinutes(5).isSameDay(latestStart)) {
getJourneys(latestStart.addMinutes(5), journeys);
} else {
setLoading(false);
}
}
}, [journeys]);
I can't use a variable like isMounted = true in the useEffect beacuse it would reach inside the if statement and reach a "setState" by the time i'm on another page.
Moving the entire call into a useEffect doesn't seem to work either. I am at a loss.
Create a variable called mounted with useRef, initialised as true. Then add an effect to set mounted.current to false when the component unmounts.
You can use mounted.current anywhere inside the component to see if it's mounted, and check that before setting any state.
useRef gives you a variable you can mutate but which doesn't cause a rerender.
When you use useEffect hook with action which can be done after component change you should also take care about clean effect when needed. Maybe example help you, also check this page.
useEffect(() => {
let isClosed = false
const fetchData = async () => {
const data = await response.json()
if ( !isClosed ) {
setState( data )
}
};
fetchData()
return () => {
isClosed = true
};
}, []);
In your use case, you probably want to create a Store that doesn't reload everytime you change route (client side).
Example of a store using useContext();
const MyStoreContext = createContext()
export function useMyStore() {
const context = useContext(MyStoreContext)
if (!context && typeof window !== 'undefined') {
throw new Error(`useMyStore must be used within a MyStoreContext`)
}
return context
}
export function MyStoreProvider(props) {
const [ myState, setMyState ] = useState()
//....whatever codes u doing with ur hook.
const exampleCustomFunction = () => {
return myState
}
const getAllRoutes = async (mydestination) => {
return await getAllMyRoutesFromApi(mydestination)
}
// you return all your "getter" and "setter" in value props so you can use them outside the store.
return <MyStoreContext.Provider value={{ myState, setMyState, exampleCustomFunction, getAllRoutes }}>{props.children}</MyStoreContext.Provider>
}
You will wrap the store around your entire App, e.g.
<MyStoreProvider>
<App />
</MyStoreProvider>
In your page where you want to use your hook, you can do
const { myState, setMyState, exampleCustomFunction, getAllRoutes } = useMyStore()
const onClick = async () => getAllRouters(mydestination)
Considering if you have client side routing (not server side), this doesn't get reloaded every time you change your route.

How to prevent infinite re-rendering with useEffect() in React [duplicate]

This question already has answers here:
react useEffect comparing objects
(7 answers)
Closed 2 years ago.
I have an app that checks the user id on startup and loads the list of todo items based on the user logged in. I have the useEffect change only when data changes, but I have setData in the body of useEffect() meaning data changes and it re-runs infinitum.
However if I change [data] to [] in the second parameter, then it renders once BUT I have to refresh the page everytime I add a todo item for it to render rather than it render automatically. How can I have it render automatically without looping infinitely?
const [data, setData] = useState([])
useEffect(() => {
UserService.getUserById(localStorage.getItem("userId")).then(res => {
if (res.data !== null) {
setData(res.data.todos)
}
})
}, [data])
You can add a condition in the call back function that checks if a certain condition is met, e.g. if data is empty. If it is empty, then fetch data, otherwise do nothing. This will prevent the infinite loop from happening.
const getData = useEffect(()=>{
const fetchData = () => {
UserService.getUserById(localStorage.getItem("userId"))
.then(res => {
if (res.data !== null) {
setData(res.data.todos)
}
})
.catch(error => {
// do something with error
})
}
if (data.length === 0)
fetchData()
},[data]);
Alternatively, you use an empty dependency array so that the callback function in the useEffect is called once.
useCallback Hook can be used with slight modifications in your code.
You will need to import useCallback from "react" first.
import {useCallback} from "react";
And then use this useCallback around our getData function. (Have modified the answer a bit)
const getData = useCallback(()=>{
UserService.getUserById(localStorage.getItem("userId")).then(res => {
if (res.data !== null) {
setData(res.data.todos)
}
})
},[data]);
useEffect(() => {
getData();
}, [data])
This React Hook will make sure that the getData() function is only created when the second argument data changes.
In your code UserService.getUserById(localStorage.getItem("userId")) return a promise and it get data one time so you just have to call getUserById one time at the time of load by using [] and if you want to call it again make a function and use it wherever on refresh function or on adding todos item or update or delete function. Otherwise you have to use observable or useCallBack hook
You need to pass the reset param to prevent loop. once callback trigger reset value false. so that execution not running again until reset the value
Codesanbox
export default function App() {
let i = 1;
const [data, setData] = useState([]);
const [reset, setReset] = useState(true);
useEffect(() => {
if (reset) {
setTimeout(() => {
//callback
setReset(false);
setData(Math.random());
}, 1000);
}
}, [data]);
return (
<div className="App">
<h1>{data}</h1>
<button
onClick={() => {
setReset(true);
setData("");
}}
>
Click this and see the data render again. i just reset the data to empty
</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
Use a condition to stop the loop by setting a condition to stop it. You can check if a certain value is set or check if there are any values sent at all.

useEffect break array of useState at first time

I am learning react hooks. I am having mock data js call "MockFireBase.js" as below:
const userIngredientsList = [];
export const Get = () => {
return userIngredientsList;
}
export const Post = (ingredient) => {
ingredient.id = userIngredientsList.length + 1;
userIngredientsList.push(ingredient);
return ingredient;
}
Then my react hooks component "Ingredients.js" will call this mock utilities as following details:
const Ingredients = () => {
const [userIngredients, setUserIngredients] = useState([]);
// only load one time
useEffect(() => { setUserIngredients(Get()); }, []);
const addIngredienHandler = ingredient => {
let responsData = Post(ingredient);
setUserIngredients(preIngredients => {
return [...preIngredients, responsData]
});
}
return (
<div className="App">
<IngredientForm onAddIngredient={addIngredienHandler} />
<section>
<IngredientList ingredients={userIngredients} />
</section>
</div>
);
)
}
When I added first ingredient, it added two (of course I get same key issue in console.log). Then I added second ingredient is fine.
If I remove the useEffect code as below, it will work good.
// only load one time
useEffect(() => { setUserIngredients(loadedIngredients); }, []);
I am wondering what I did anything wrong above, if I use useEffect
The problem is not in useEffect. It's about mutating a global userIngredientsList array.
from useEffect you set initial component state to be userIngredientsList.
Then inside addIngredienHandler you call Post(). This function does two things:
2a. pushes the new ingredient to the global userIngredientsList array`. Since it's the same instance as you saved in your state in step 1, your state now contains this ingredient already.
2a. Returns this ingredient
Then, addIngredienHandler adds this ingredient to the state again - so you end up having it in the state twice.
Fix 1
Remove userIngredientsList.push(ingredient); line from your Post function.
Fix 2
Or, if you need this global list of ingredients for further usage, you should make sure you don't store it in your component state directly, and instead create a shallow copy in your state:
useEffect(() => { setUserIngredients([...Get()]); }, []);

Categories

Resources