Why is react useState not updating the values? [duplicate] - javascript

This question already has answers here:
The useState set method is not reflecting a change immediately
(15 answers)
Best way to request unknown number of API pages in useEffect hook
(4 answers)
Closed 2 years ago.
I am using an API to fetch data, but in order to fetch all the data I am required to loop through the links
const [characterData, setCharacterData] = useState([]);
const [link, setLink] = useState("https://swapi.dev/api/people/");
useEffect(() => {
getData();
}, [])
async function getData() {
while (link) {
const fetchedData = await fetch(link);
const jsonData = await fetchedData.json();
setCharacterData([...characterData, jsonData.results]);
setLink(jsonData.next);
console.log(link)
}
}
This is how one of the jsonData from above would look like:
{
"next": "http://swapi.dev/api/people/?page=2", "previous": null, "results": [list of the wanted data] }
The last object will have "next": null, so the while loop should end at some point, but for some reason setLink() never updates the link and causes it to become an infinite loop. Link always remains as "https://swapi.dev/api/people/".
Another problem is that the page isn't displaying anything as characterData gets updated, so I am assuming that characterData isn't updating either, but I am not sure.
characterData.map((character, index) => {
return (
<div className="character-item" key={index}>
<h4>{character.name}</h4>
<p>{character.birth_year}</p>
</div>
);
})
Note: Each character is an object
Thank you!

The link is declared with const - it'll never change in a given render. So the
while (link) {
will run forever.
Unless you're using the link elsewhere, I'd remove it from state entirely and use a local variable inside getData instead. Make sure to use the callback form of setCharacterData too so that you don't lose the prior value in state.
async function getData() {
let link = 'https://swapi.dev/api/people/';
while (link) {
const fetchedData = await fetch(link);
const jsonData = await fetchedData.json();
setCharacterData(characterData => [...characterData, jsonData.results]);
link = jsonData.next;
}
}
It would also be a great idea to catch possible errors:
useEffect(() => {
getData()
.catch(handleErrors);
}, [])

Related

How to run React custom hook conditionally or inside a function

I have a custom hook to redirect users to edit page. On index page I can duplicate items and delete. I can redirect users after duplicate, but the problem is when I delete an item, this custom hook redirects users to edit page again. So I need to find a way to make it work conditionally.
Custom hook:
export default function useDuplicateItem(url: string) {
const { sendRequest: duplicate } = useHttpRequest();
const duplicateItem = useCallback(
(data) => {
duplicate([
{
url: `/api/server/${url}`,
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
data,
},
]);
},
[duplicate, url]
);
useRedirectEditPage(url); // This causes the problem
return duplicateItem;
}
index page:
const duplicateItem = useDuplicateItem('documents');
// custom hook rendered here, which is not correct. I want to run it when duplicate function runs.
const duplicate = useCallback(() => {
const data = {
name: copiedName,
sources: singleDocument?.sources,
document: singleDocument?.document,
tool: singleDocument?.tool,
access: singleDocument?.access,
};
duplicateItem(data);
}, [copiedName, duplicateItem, singleDocument]);
useRedirectEditPage:
export default function useRedirectEditPage(slug: string) {
const { saveResponses, setSaveResponses, setHeaderStates } =
useAdminContext();
const router = useRouter();
useEffect(() => {
const statusCodes: number[] = [];
let id;
saveResponses.forEach((value) => {
statusCodes.push(value?.status);
id = value?.id;
});
if (statusCodes.length && id) {
if (statusCodes.includes(404)) {
setHeaderStates((prev) => ({
...prev,
canBeSaved: false,
}));
} else {
router.push(`/admin/${slug}/edit/${id}`);
setSaveResponses(new Map());
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [saveResponses, router, setSaveResponses]);
}
saveResponses state is coming after I make any request to server, and I am able to get id to redirect users. I use new Map() to set data inside saveResponses.
From the react docs:
Don’t call Hooks inside loops, conditions, or nested functions.
Instead, always use Hooks at the top level of your React function,
before any early returns. By following this rule, you ensure that
Hooks are called in the same order each time a component renders.
That’s what allows React to correctly preserve the state of Hooks
between multiple useState and useEffect calls. (If you’re curious,
we’ll explain this in depth below.)
React relies on the order in which Hooks are called to know which setState corresponds to which state, calling them inside a condition will mess up the previous mechanism.
I would recommend to read the following: https://reactjs.org/docs/hooks-rules.html#explanation

Why do I see stale data even after invalidating my queries?

I have created a function which adds a specific item to my diary. 9/10 times everything works, which means that there is nothing wrong with the code?
However rarely I add the item to my diary, but I don't see the update values, even thought I activated queryClient.invalidateQueries() method, the value is updated on my server, because when I manually refresh I see the updated diary again.
Does this mean that by the time I activate invalidatequeries method, the update has not reached my server and that is why I am seeing stale data? But what would I do in that case?
Here is the function:
const newAddItemFunction = () => {
const day = newDiary?.[currentDay];
if (day && selectedMealNumber && selectedItem) {
setSavingItem(true);
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
router.push("/diary");
}
};
Here is my custom hook(useFirestoreQuery is just custom wrapped useQuery hook for firebase):
export const useGetCollectionDiary = () => {
const user = useAuthUser(["user"], auth);
const ref = collection(
firestore,
"currentDiary",
user.data?.uid ?? "_STUB_",
"days"
);
return useFirestoreQuery(
["currentDiary"],
ref,
{
subscribe: false,
},
{
select: (data) => {
let fullDaysArray = [] as Day[];
data.docs.map((docSnapshot) => {
const { id } = docSnapshot;
let data = docSnapshot.data() as Day;
data.documentId = id;
fullDaysArray.push(data);
});
fullDaysArray.sort((a, b) => a.order - b.order);
return fullDaysArray;
},
enabled: !!user.data?.uid,
}
);
};
NewAddItemToDiary function is just firebase call to set document:
//...json calculations
setDoc(
doc(
firestore,
"currentDiary",
auth.currentUser.uid,
"days",
day.documentId
),
newDiaryWithAddedItem
);
9/10 times everything works, which means that there is nothing wrong with the code?
It indicates to me that there is something wrong with the code that only manifests in edge cases like race conditions.
You haven't shared the code of what NewAddItemToDiary is doing, but I assume it's asynchronous code that fires off a mutation. If that is the case, it looks like you fire off the mutation, and then invalidate the query without waiting for the query to finish:
NewAddItemToDiary({
day,
selectedMealNumber,
selectedItem,
});
queryClient.invalidateQueries(["currentDiary"]).then(() => {
toast.success(`${selectedItem.product_name} has been added`);
});
Mutations in react-query have callbacks like onSuccess or onSettled where you should be doing the invalidation, or, if you use mutateAsync, you can await the mutation and then invalidate. This is how all the examples in the docs are doing it:
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation(addTodo, {
onSuccess: () => {
queryClient.invalidateQueries('todos')
queryClient.invalidateQueries('reminders')
},
})

Fetching data using a async function in react [duplicate]

This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed last year.
I have been trying to create a table from an array of objects. I'm able to console the data inside the async function. But I couldn't console it outside.
My code :
useEffect(() => {
listingCampaignsModels()
}, []);
async function listingCampaignsModels() {
const apiData = await DataStore.query(Campaign);
console.log(apiData);
console.log(typeof(apiData));
return apiData;
};
When I tried to console apiData outside, it returns apiData is not defined error.
The data looks like this :
[Model, Model, Model, Model, Model, Model]
Each Model looks like :
-> 1: Model {id: 'c40d6b22-840f-467a-909c-7b2b19960ffb', campaignOwner: 'eumagnas', campaignName: "mollitlab", startDate: "2022/08/15", endDate: "2022/10/25", expectedRevenue: 25, budgetedCost: 27, actualCost: 28}
I want loop through all the Models and create a table as :
Campaign Owner
Campaign Name
Start Date
End Date
Expected Revenue
Budgeted Cost
Actual Cost
eumagnas
mollitlab
2022/08/15
2022/10/25
25
27
28
You need to use useState() hook to save the data as campaign state.
const [campaigns, setCampaigns] = useState([]);
useEffect(() => {
listingCampaignsModels()
}, []);
async function listingCampaignsModels() {
const apiData = await DataStore.query(Campaign);
console.log(apiData);
setCampaigns(apiData);
};

state update from a callback

The following member function populates asynchronously a folder_structure object with fake data:
fake(folders_: number, progress_callback_: (progress_: number) => void = (progress_: number) => null): Promise<boolean>
{
return new Promise((resolve, reject) => {
for (let i_ = 0; i_ < folders_; i_++) {
progress_callback_(i_ / folders_ * 100.);
this.add(this.id(), faker.address.city() + i_, random_choice(this.folder_structure_id()));
}
progress_callback_(folders_ / folders_ * 100.);
resolve(true);
})
}
It uses a callback to update the progress within the for loop which is then used to update the state (a progress bar) from within a useEffect() function with an empty dependency array.
let [progress_state_, set_progress_state_] = useState<number>(0);
let [fake_done_, set_fake_done_] = useState<boolean>(false);
useEffect(() =>
{
if (fake_)
folder_structure_.fake(fake_, (progress_) => {
set_progress_state_(progress_)
}).then(value => set_fake_done_(value));
}, [])
if (!fake_ || fake_done_) etc etc
However, the state is not updated (logging the progress in the console seems to work fine). Any ideas as to whether it's possible to update a state from within useEffect?
The reason your useEffect hook isn't working is that it's not called upon progress_state_ state change.
Instead of
useEffect(() =>
{
...
}, [])
Try this instead
useEffect(() =>
{
...
}, [progress_])
Adding progress_ to the dependency array means useEffect will be called every single time progress_ changes. If you leave it as an empty dependency array, then useEffect is only ever called in the very beginning on when the code is mounted to the DOM.
Here's a good explanation on dependency arrays: https://devtrium.com/posts/dependency-arrays
Addressing your final question: Yes, it is possible to update state from within useEffect.
To understand the root of your main issue, I would be curious to see how you are doing your logging. Are you logging from within fake() or from your render() function?

useEffect() doesn't trigger on property object change (Deep object)

I have a component which takes a deep object as argument. I want to be able to dynamically alter my component based on the property that I pass - But for some reason my useEffect() loop doesn't run the 2nd time that I pass an object, which naturally is due to the fact that it is a deep object which useEffect() doesn't "recognize"
My code is as follows:
<MyComponent groups={sourceArray}/>
function MyComponent({ groups }) {
useEffect(() => {
//Do something
}, [groups])
For clarification, my data is an array of objects.
I did try several things to get this to work - but for some reason, I just cant get my effect loop to trigger:
First alternative solution which doesn't work (useRef)
useEffect(() => {
if (prevGroups && !isEqual(prevGroups, groups)) {
// do something
}
}, [groups])
const usePrevious = (value) => {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
};
const prevGroups = usePrevious(groups);
2nd solution which also fails (External library)
I tried using the following library, - Also without any luck.
Conclusively, I'm unsure what's going on here and how I would get my useEffect loop to run on update.
The easy solution would be to remove the dependency array - But that just makes the useEffect loop run infinitely
Update
object has the following structure:
[{"name": "123", "id": "1", "children": [{...}]}]
Also, I'm passing the value as a useState(), i.e.:
const funcToChangeObj = () => {
//logic to change sourceArray
setSourceArray(changedArray)
}
setSourceArray(changedArray) -> setSourceArray(changedArray.slice())

Categories