React useffect infinite render or stale state - javascript

I am getting some posts from the server and every time i delete or add a post, i need to refresh the page for it to showcase the changes. This can be solved by adding posts in the dependency array but then an infinite render occurs.
useEffect(() => {
const getUsersData = async () => {
const results = await getUsers();
setUsers(results.data);
console.log(results.data);
};
const getPostData = async () => {
const results = await getPosts();
console.log(results.data);
setPosts(
results.data.sort((p1, p2) => {
return new Date(p2.created_at) - new Date(p1.created_at);
})
);
};
getUsersData();
getPostData();
}, [posts]);
{post.user_id === user.result.user_id && (
<DeleteIcon
color="secondary"
onClick={() =>
handleDelete(post.post_id, { user_id: user.result.user_id })
}
/>
)}
__
export const deletePost = async (postId, userId) => {
await axios.delete(`${URL}/posts/${postId}`, { data: userId });
};

useEffect(() => {
const getPostData = async () => {
...
setPosts(
...
);
};
getPostData();
}, [posts]);
Oops !
The issue is here, you are setting your post each time you... are setting your posts !
Maybe you should use setpost somewhere else in your code ? :)
If you want to update your posts, you should do it in another useeffect, with whatever dependencies you need to know you need to update your poste. Or do a timed refresh, also in a use effect. You can then call setpost, without having access to post. You don't need post as dependency to update it, on the contrary, that's what's causing a loop here :)

You can't add posts as a dependency to your useEffect ince you are using that effect to call setPosts. This will cause an infinite rerender loop.
Your issue is one of the reasons why many fetch libraries for react have been created in the last few years, like react-query, or RTK query, because what you want to do, is to update your queried data, in response to a mutation on the same data-set on server ( mutations: POST, DELETE, PATCH, PUT ). These libraries let you specify what query data to revalidate once you perform a mutation, in your case, you would tell your getPosts query to be reexecuted and revalidated in cache everytime you perform an addPost or deletePost mutation.
If you want to implement manually both optimistic update and cache revalidation, you need to add some more code, you will have basically these code blocks:
const [posts, setPosts] = useState([])
const getUsersData = async () => {
const results = await getUsers();
setUsers(results.data);
console.log(results.data);
};
const getPostsData = async () => {
const results = await getPosts();
console.log(results.data);
setPosts(results.data.sort((p1, p2) => {
return new Date(p2.created_at) - new Date(p1.created_at);
})
);
};
const handleDelete = async (postId, userId) => {
await deletePost(postId, userId) // DELETE Call
const idx = posts.findIndex(p => p.id === postId )
setPosts(posts => [...posts.slice(0, idx),...posts.slice(idx+1, posts.length)] // Optimistic update of UI
getPosts() // Revalidate posts after the delete operation
}
const handleAddPost = async (post, userId) => {
await addPost(post, userId) // POST Call
setPosts(posts => [post,...posts] // Optimistic update of UI
getPosts() // Revalidate posts after the POST operation
}
// Retrieve data on the first component mount
useEffect(() => {
getUsersData();
getPostData();
}, []);

Related

Passing external data into components

In my react app, I am currently passing a list of stores by calling the API directly from the URL.
const getStore = async () => {
try {
const response = axios.get(
'http://localhost:3001/appointment-setup/storeList'
);
return response;
} catch (err) {
console.error(err);
return false;
}
};
I pass this function into my useEffect hook where I would set my get a list of stores using resp.data.stores:
const [storeLocations, setStoreLocations] = useState([]);
useEffect(() => {
async function getData(data) {
await service.stepLocation.init();
const resp = await getStore();
setStoreLocations(resp.data.stores);
}
setFlagRender(true);
return getData();
}, []);
This works, however, I noted in useEffect there is a call await service.stepLocation.init(). There is a file that already takes care of all the backend/data for the component.
const stepLocation = {
// removed code
// method to retrieve store list
retrieveStoreList: async function ()
let response = await axios.get(
constants.baseUrl + '/appointment-setup/storeList'
);
return response.data.stores;
,
// removed code
Since this data is available, I don't need the getStore function. However when I try to replace response.data.stores in useEffect with service.stepLocation.retrieveStoreList no data is returned. How do I correctly pass the data from this file in my useEffect hook?
I think your useEffect should be like follows as you want to save the stores in your state.
useEffect(() => {
const updateStoreLocations = async () => {
const storeLocations = await service.stepLocation.retrieveStoreList();
setStoreLocations(storeLocations);
}
updateStoreLocations();
}, [])

Async function inside useMemo not working

I have a situation in which I have an object with all the books and I want to get the author info which sits in a different collection. I tried fetching the data inside a useMemo, but I get an error since the promise does not get resolved I guess. How to make useMemo wait for the data to come for the author?
async function useAuthor(authorID:string) {
await firestore.collection('users').doc(authorID).get().then(doc => {
return doc.data();
})
};
const normalizedBooks = useMemo(
() =>
books?.map( (book) => ({
...book,
author: useAuthor(book.authorId),
})),
[books]
);
Fetching remote data should be done in an effect:
function useAuthor(authorID:string) {
const [author, setAuthor] = useState(null);
useEffect(() => {
firestore.collection('users').doc(authorID).get().then(
doc => setAuthor(doc.data())
);
}, [authorID]);
return author;
}
Note that author will be null during the first render and until the request completed. You can improve that example by e.g. adding loading and error states. Or you can also use something like react-query which provides those out of the box:
import useQuery from 'react-query';
function getItemById(collectionID:string, itemID:string) {
return firestore.collection(collectionID).doc(itemID).get().then(
doc => doc.data()
);
}
const Author = ({authorID}) => {
const {data: author, isLoading, error} = useQuery(
['users', authorID],
getItemById
);
if (isLoading) return 'Loading...'
if (error) return 'Failed to fetch author :(';
return <p>{author.name}</p>;
}

setState in nested async function - React Hooks

How can I build a function which gets some data asynchronously then uses that data to get more asynchronous data?
I am using Dexie.js (indexedDB wrapper) to store data about a direct message. One thing I store in the object is the user id which I'm going to be sending messages to. To build a better UI I'm also getting some information about that user such as the profile picture, username, and display name which is stored on a remote rdbms. To build a complete link component in need data from both databases (local indexedDB and remote rdbms).
My solution returns an empty array. It is being computed when logging it in Google Chrome and I do see my data. However because this is not being computed at render time the array is always empty and therefor I can't iterate over it to build a component.
const [conversations, setConversations] = useState<IConversation[]>()
const [receivers, setReceivers] = useState<Profile[]>()
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result)
})
}, [])
useEffect(() => {
if (conversations) {
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
// the above await is a javascript fetch call to my backend that returns json about the user values I mentioned
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
}, [conversations])
/*
The below log logs an array with a length of 0; receivers.length -> 0
but when clicking the log in Chrome I see:
[
0: {
avatarURL: "https://lh3.googleusercontent.com/..."
displayName: "Cool guy"
userId: "1234"
username: "cool_guy"
}
1: ...
]
*/
console.log(receivers)
My plan is to then iterate over this array using map
{
receivers && conversations
? receivers.map((element, index) => {
return <ChatLink
path={conversations[index].path}
lastMessage={conversations[index].last_message}
displayName={element.displayName}
username={element.username}
avatarURL={element.avatarURL}
key={index}
/>
})
: null
}
How can I write this to not return a empty array?
Here's a SO question related to what I'm experiencing here
I believe your issue is related to you second useEffect hook when you attempt to do the following:
const getReceivers = async () => {
let receivers: Profile[] = []
await conversations.forEach(async (element) => {
const receiver = await getProfileById(element.conversationWith, token)
receivers.push(receiver)
})
return receivers
}
getReceivers().then(receivers => {
setReceivers(receivers)
})
}
Unfortunately, this won't work because async/await doesn't work with forEach. You either need to use for...of or Promise.all() to properly iterate through all conversations, call your API, and then set the state once it's all done.
Here's is a solution using Promise.all():
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
const receivers: Profile[] = await Promise.all(
conversations.map(conversation =>
getProfileById(element.conversationWith, token)
)
);
setReceivers(receivers);
}
getReceivers()
}, [conversations]);
// NOTE: You don't have to do the `receivers && conversations`
// check, and since both are arrays, you should check whether
// `receivers.length !== 0` and `conversations.length !== 0`
// if you want to render something conditionally, but since your
// initial `receivers` state is an empty array, you could just
// render that instead and you won't be seeing anything until
// that array is populated with some data after all fetching is
// done, however, for a better UX, you should probably indicate
// that things are loading and show something rather than returning
// an empty array or null
return receivers.map((receiver, idx) => <ChatLink />)
// or, alternatively
return receivers.length !== 0 ? (
receivers.map((receiver, idx) => <ChatLink />)
) : (
<p>Loading...</p>
);
}
Alternatively, using for...of, you could do the following:
function App() {
const [conversations, setConversations] = useState<IConversation[]>([]);
const [receivers, setReceivers] = useState<Profile[]>([]);
useEffect(() => {
messagesDatabase.conversations.toArray().then(result => {
setConversations(result);
});
}, []);
useEffect(() => {
if (conversations.length === 0) {
return;
}
async function getReceivers() {
let receivers: Profile[] = [];
const profiles = conversations.map(conversation =>
getProfileById(conversation.conversationWith, token)
);
for (const profile of profiles) {
const receiver = await profile;
receivers.push(receiver);
}
return receivers;
}
getReceivers().then(receivers => {
setReceivers(receivers);
});
}, [conversations]);
return receivers.map((receiver, idx) => <ChatLink />);
}
i think it is happening because for getReceivers() function is asynchronous. it waits for the response, in that meantime your state renders with empty array.
you can display spinner untill the response received.
like
const[isLoading,setLoading]= useState(true)
useEffect(()=>{
getReceivers().then(()=>{setLoading(false)}).catch(..)
} )
return {isLoading ? <spinner/> : <yourdata/>}
Please set receivers initial value as array
const [receivers, setReceivers] = useState<Profile[]>([])
Also foreach will not wait as you expect use for loop instead of foreach
I am not sure it is solution for your question
but it could help you to solve your error

How delay redux saga action to store data update?

Can't delay action firing (initialize from redux-form) to store data update after fetching.
Store is initializing with empty account object.
At initial render getAccount action firing and triggering update of store.
useEffect see store updating and triggering getAccount action second time
second data request
END
const {getAccount, initialize} = props
prepareData = () => {...prepared obj}
useEffect(() => {
const begin = async () => {
await getAccount();
await initialize(prepareData());
};
begin();
}, [account.id]);
main aim to avoid unnecessary second request
What if you put a conditional on the begin call?
const {getAccount, initialize} = props
const {begun, setBegun} = useState(false)
prepareData = () => {...prepared obj}
useEffect(() => {
const begin = async () => {
await setBegun(true);
await getAccount();
await initialize(prepareData());
};
begun || begin();
}, [account.id]);
Should getAccount be called if account.id doesn't exist? If not then simply check that account.id exists before calling begin.
const {getAccount, initialize} = props
prepareData = () => {...prepared obj}
useEffect(() => {
// add a guard condition to exit early if account.id doesn't exist yet
if (!account.id) {
return;
}
const begin = async () => {
await getAccount();
await initialize(prepareData());
};
begin();
}, [account.id]);

React state not updating after async await call

I am using React Hooks and the useEffect to get data from an API. I need to make 2 consecutive calls. If I console.log the response directly, it shows the correct response, but when I console.log the state, its empty and its not updating. What am I doing wrong?
const [describeFields, setDescribeFields] = useState([]);
useEffect(() => {
const fetchData = async () => {
await axios
.all([
axios.get("someUrl"),
axios.get("someotherUrl")
])
.then(
axios.spread((result1, result2) => {
console.log(result1.data.result.fields); //shows correct response
setDescribeFields(result1.data.result.fields);
console.log(describeFields); //shows empty array
})
);
};
fetchData();
}, []);
It will not be displayed just after that if you want to see latest changes just console log before return statement as setState is async your console.log run before then setState
const [describeFields, setDescribeFields] = useState([]);
useEffect(() => {
const fetchData = async () => {
await axios
.all([
axios.get("someUrl")
axios.get("someotherUrl")
])
.then(
axios.spread((result1, result2) => {
console.log(result1.data.result.fields); //shows correct response
setDescribeFields(result1.data.result.fields);
})
);
};
fetchData();
}, []);
console.log(describeFields); //Check here
return(
)
Note : [] array in useEffect means it will run only one time. If you want to update as long as variable changes then add it as dependency.
Here is working example : https://codesandbox.io/s/prod-thunder-et83o

Categories