React-query setQueryData not re-rendering component - javascript

I've been dealing for a while with this problem and still can't tackle it.
I'm using React-query as a server state management library and I'm trying to get my UI state synchronized with my server state when a mutations occurs. Since I can use the mutation response to avoid a new API call, I'm using the setQueryData feature that React-query gives us.
The problem is that the old-data is being correctly modified (I can see it in the react-query DevTools) when a mutation is successful, but the component using it isn't being re-rendered, making my UI State not synchronized with my Server state (well, at least the user can't see the update).
Let me show some code and hope someone can give me some insights.
Component using the query:
const Detail = ({ orderId }) => {
const { workGroups } = useWorkGroups();
const navigate = useNavigate();
const queryClient = useQueryClient();
const orderQueries = queryClient.getQueryData(["orders"]);
const queryOrder = orderQueries?.find((ord) => ord.id === orderId);
// more code
Component mutating the query:
const Deliver = ({
setIsModalOpened,
artisan,
index,
queryQuantity,
queryOrder,
}) => {
const [quantity, setQuantity] = useState(() => queryQuantity);
const { mutate: confirmOrderDelivered } = useMutateOrderDeliveredByArtisan(
queryOrder.id
);
const onSubmit = () => {
confirmOrderDelivered(
{
id: queryOrder.artisan_production_orders[index].id,
artisan: artisan.user,
items: [
{
quantity_delivered: quantity,
},
],
},
{
onSuccess: setIsModalOpened(false),
}
);
};
// more code
Now the mutation function (ik it's a lot of logic but I dont' want to refetch the data using invalidateQueries since we're dealing with users with a really bad internet connection). Ofc you don't need to understand each step of the fn but what it basically does is update the old queried data. In the beginning I thought it was a mutation reference problem since React using a strict comparison under the hood but I also checked it and It doesn't look like it's the problem. :
{
onSuccess: (data) => {
queryClient.setQueryData(["orders"], (oldQueryData) => {
let oldQueryDataCopy = [...oldQueryData];
const index = oldQueryDataCopy.findIndex(
(oldData) => oldData.id === orderId
);
let artisanProdOrders =
oldQueryDataCopy[index].artisan_production_orders;
let artisanProductionOrderIdx = artisanProdOrders.findIndex(
(artProdOrd) => artProdOrd.id === data.id
);
artisanProdOrders[artisanProductionOrderIdx] = {
...artisanProdOrders[artisanProductionOrderIdx],
items: data.items,
};
const totalDelivered = artisanProdOrders.reduce((acc, el) => {
const delivered = el.items[0].quantity_delivered;
return acc + delivered;
}, 0);
oldQueryDataCopy[index] = {
...oldQueryDataCopy[index],
artisan_production_orders: artisanProdOrders,
items: [
{
...oldQueryDataCopy[index].items[0],
quantity_delivered: totalDelivered,
},
],
};
return oldQueryDataCopy;
});
},
onError: (err) => {
throw new Error(err);
},
}
And last but not least: I already checked that the oldQueryData is being correctly modified (console loging in the onSuccess fn in the mutation response) and, as I said before, the data is correctly modified in the React-query DevTools.
I know this is a lot of code and the problem seems to be complex but I really believe that it might be a really easy thing that I'm not pointing out because of how tired I already am.
Thanks!

Well, I fixed it in the worst possible way imho, so I will answer this question but I really would like to read your thoughts.
It looks like the new query data setted on the expected query is re-rendering the component only if the mutation function is located in the component that we actually want to re-render.
With that in mind what I did was just colocate my mutation function in the parent component and pass it down through the child component.
Something like this:
const Detail = ({ orderId }) => {
const { workGroups } = useWorkGroups();
const navigate = useNavigate();
const { mutate: confirmOrderDelivered } = useMutateOrderDeliveredByArtisan(
queryOrder.id
); ==============> THE MUTATION FN IS NOW IN THE PARENT COMPONENT
const queryClient = useQueryClient();
const orderQueries = queryClient.getQueryData(["orders"]);
const queryOrder = orderQueries?.find((ord) => ord.id === orderId);
// more code
First child:
const Assigned = ({ artisan, queryOrder, index, confirmOrderDelivered }) => {
// THE IMPORTANT PART HERE IS THE PROP BEING PASSED DOWN.
<Modal
isOpen={isModalOpened}
toggleModal={setIsModalOpened}
// className="w312"
width="80%"
height="fit-content"
justifyCont="unset"
alignItems="unset"
>
<Deliver
setIsModalOpened={setIsModalOpened}
artisan={artisan}
queryQuantity={quantity}
queryOrder={queryOrder}
index={index}
confirmOrderDelivered={confirmOrderDelivered} => HERE
/>
</Modal>
Component that actually needs the mutation fn:
const Deliver = ({
setIsModalOpened,
artisan,
index,
queryQuantity,
queryOrder,
confirmOrderDelivered,
}) => {
const [quantity, setQuantity] = useState(() => queryQuantity);
const onSubmit = () => {
confirmOrderDelivered( => HERE.
{
id: queryOrder.artisan_production_orders[index].id,
artisan: artisan.user,
items: [
{
quantity_delivered: quantity,
},
],
}
);
};

You can't mutate any prop.
You always need to create new versions of the objects and props and use destructuring.
Example
queryClient.setQueryData([QUERY_KEYS.MYKEY], (old) => {
const myArray = [...old.myArray];
return {
...old,
// WRONG
myArray[0].name: 'text1',
// CORRECT
myArray[0]: {
...myArray[0],
name: 'text1'
}
}})

Related

React state not giving correct value on useEffect()

I'm trying to build a factorization algorithm using react. I would like to add results to LocalStorage based on results from factorization. However, LocalStorage sets previous results not current ones.
I think this is happening because useEffect runs on every new [number] (=user input) and not based on [results]. However, I need useEffect to run on new user input submition because that's when factorization has to be triggered.
How could I make localStorage set correct results after that factorization has completed (on the finally block if possible) ?
const [results, setResults] = useState({
default: [],
detailed: [],
isPrime: '',
});
const { number } = props
useEffect(() => {
handleWorker(number);
//accessing results here will give previous results
}, [number]);
const handleWorker = number => {
try {
const worker = new Worker(new URL('facto.js', import.meta.url));
worker.postMessage({ number, algorithm });
worker.onmessage = ({ data: { facto } }) => {
setResults({ ...facto });
//the worker 'streams live' results and updates them on setResults
};
} catch(error) {
console.log(error.message)
} finally {
localStorage.setItem(number, results)
//here, results is not current state but previous one
}
};
Please note that everything else works fine
Thanks
You are getting the previous value because localStorage.setItem is executed before setResults updates the state. Yo can do some refactor to make it work:
const [results, setResults] = useState({
default: [],
detailed: [],
isPrime: '',
});
const { number } = props;
const workerRef = useRef(new Worker(new URL('facto.js', import.meta.url)));
const worker = workerRef.current;
useEffect(()=> {
//-> create the listener just once
worker.onmessage = ({ data: { facto } }) => {
setResults({ ...facto });
};
}, []);
useEffect(() => {
//-> send message if number changed
worker.postMessage({ number, algorithm });
}, [number]);
useEffect(() => {
//-> update localStorage if results changed
localStorage.setItem(number, results)
}, [results]);
Here is what you need (probably):
const [results, setResults] = useState({ /* YOUR STATE */ });
const { number } = props
const handleWorker = useCallback((number) => {
// DO WHATEVER YOU NEED TO DO HERE
},[]); // IF YOU DEPEND ON ANY MUTABLE VARIABLES, ADD THEM TO THE DEPENDENCY ARRAY
useEffect(() => {
// DO WHATEVER YOU NEED TO DO HERE
// INSTEAD OF localStorage.setItem(number, results)
// DO localStorage.setItem(number, { ...facto })
}, [number,handleWorker]);
Let me know if it works.
I think you can pass in the result state as dependent for the useEffect, so whenever the value of the result change , useEffect runs
useEffect(() => { //code goes here}, [results]);
Let say results also depends on a function handleWorker
useEffect(() => { //code goes here},
[results,handleWorker]);

How do I merge the results of two Firestore Realtime listeners into one array of objects using React

I am trying to merge the data received from two real-time listeners into one requests array. The array contains objects as its elements. This is so I can pass one single requests prop and manipulate that in my Dashboard component.
In Firestore, my documents in my Requests collection contain employeeUser and managerUser as references to another Users collection which contains information about the user such as their username. So I am also trying to receive their username within my snapshot as well.
Here is an example of the fields in the document of the Requests collection:
I am having issues merging the data from both listeners into one requests array using useEffect and useState hooks. Also, when I try to use the spread syntax to merge arrays it doesn't seem to actually have an effect on setRequests. When I use a callback setRequests((prev) => [...prev, ...employeeRequests]) it doesn't seem to do anything either.
In a nutshell, with the current code I have below, I seem to be receiving the correct data in the console (only in managerRequests & employeeRequests arrays) but I cannot merge the two arrays together and it is not displaying properly after all renders have been completed.
I did also notice that while I am editing the code and make a change right after that involves requests, the hot reload re-renders the page once more and then everything displays correctly. Once I refresh the page, nothing displays again until I have another hot reload that affects setRequests. I am wondering if I have to trigger some more renders somehow to get all the updates in the useEffect dependency array.
Here is my code so far:
const [requests, setRequests] = useState([]); // the array where I am trying to merge both managerRequests and employeeRequests into (cause its the only way I've been able to get it to kinda work)
const [managerRequests, setManagerRequests] = useState([]);
const [employeeRequests, setEmployeeRequests] = useState([]);
useEffect(() => {
if (firebase) {
//Set up first listener
const unsubscribe = firebase.getManagerRequests({
userUid: user.uid, onSnapshot: (snapshot) => {
const managerRequests = [];
snapshot.forEach(async doc => {
const [managerPromise, employeePromise] = await Promise.all([doc.data().managerUser.get(), doc.data().employeeUser.get()]); // single get() queries to retrieve employee and manager usernames from referenced User collection
var managerUsername = managerPromise.data().username;
var employeeUsername = employeePromise.data().username;
managerRequests.push({
id: doc.id,
managerUsername: managerUsername,
employeeUsername: employeeUsername,
...doc.data()
})
})
// console.log(managerRequests) -> managerRequests contains the correct set of data in an array of objects ex: [{id:1, ...}, {id:2, ...}]
setManagerRequests(managerRequests);
setLoadingElements((vals) => ({ ...vals, managerRequestsLoading: false })) // Currently not using these as barrier flags
}
})
//Set up second listener
const unsubscribetwo = firebase.getEmployeeRequests({
userUid: user.uid, onSnapshot: (snapshot) => {
const employeeRequests = [];
snapshot.forEach(async doc => {
const [managerPromise, employeePromise] = await Promise.all([doc.data().managerUser.get(), doc.data().employeeUser.get()]);
var managerUsername = managerPromise.data().username;
var employeeUsername = employeePromise.data().username;
employeeRequests.push({
id: doc.id,
managerUsername: managerUsername,
employeeUsername: employeeUsername,
...doc.data()
})
})
// console.log(employeeRequests) > employeeRequests contains the correct set of data in an array of objects ex: [{id:1, ...}, {id:2, ...}]
setEmployeeRequests(employeeRequests);
setLoadingElements((vals) => ({ ...vals, employeeRequestsLoading: false })) // Currently not using these as barrier flags
}
})
setRequests([...managerRequests, ...employeeRequests]); // This does not seem to work. requests has nothing in it in the last render
return () => {
if (unsubscribe) {
unsubscribe();
}
if (unsubscribetwo) {
unsubscribetwo();
}
}
}
}, [firebase, user])
console.log(requests) // contains nothing in requests
return (
<>
<Navbar />
<Dashboard requests={requests} /> // This is where I want to pass an array of the merged employee and manager requests as a prop to my Dashboard component
<Footer />
</>
)
Edit: Here is how I fixed it with the help of the first answer:
const [requests, setRequests] = useState([]);
const [managerRequests, setManagerRequests] = useState([]);
const [employeeRequests, setEmployeeRequests] = useState([]);
const [managerUsername, setManagerUsername] = useState('');
const [employeeUsername, setEmployeeUsername] = useState('');
const [managerUsernameEmployeeSide, setManagerUsernameEmployeeSide] = useState('');
const [employeeUsernameEmployeeSide, setEmployeeUsernameEmployeeSide] = useState('');
useEffect(() => {
if (firebase) {
//Set up first listener
const unsubscribe = firebase.getManagerRequests({
userUid: user.uid, onSnapshot: (snapshot) => {
const managerRequests = [];
snapshot.forEach(doc => {
// Had to remove await Promise.all(...) to get it to work
doc.data().managerUser.get()
.then(res => {
setManagerUsername(res.data().username);
})
doc.data().employeeUser.get()
.then(res => {
setEmployeeUsername(res.data().username);
})
managerRequests.push({
id: doc.id,
managerUsername: managerUsername,
employeeUsername: employeeUsername,
...doc.data()
})
})
setManagerRequests(managerRequests);
}
})
return () => {
if (unsubscribe) {
unsubscribe();
}
}
}
}, [firebase, user, managerUsername, employeeUsername])
useEffect(() => {
if (firebase) {
//Set up second listener
const unsubscribetwo = firebase.getEmployeeRequests({
userUid: user.uid, onSnapshot: (snapshot) => {
const employeeRequests = [];
snapshot.forEach(doc => {
const [managerPromise, employeePromise] = await Promise.all([doc.data().managerUser.get(), doc.data().employeeUser.get()]);
var managerUsername = managerPromise.data().username;
var employeeUsername = employeePromise.data().username;
doc.data().managerUser.get()
.then(res => {
setManagerUsernameEmployeeSide(res.data().username);
})
doc.data().employeeUser.get()
.then(res => {
setEmployeeUsernameEmployeeSide(res.data().username);
})
employeeRequests.push({
id: doc.id,
managerUsername: managerUsernameEmployeeSide,
employeeUsername: employeeUsernameEmployeeSide,
...doc.data()
})
})
setEmployeeRequests(employeeRequests);
}
})
}
return () => {
if (unsubscribetwo) {
unsubscribetwo();
}
}
}, [firebase, user, managerUsernameEmployeeSide, employeeUsernameEmployeeSide]
useEffect(() => {
if (managerRequests.length > 0 && employeeRequests.length > 0) {
setTransactions([...managerRequests, ...employeeRequests]);
}
}, [managerRequests, employeeRequests])
return (
<>
<Navbar />
<Dashboard requests={requests} /> // Requests finally receives one array with all values from managerRequests and employeeRequests combined
<Footer />
</>
)
You subscribe to firebase.getManagerRequests and firebase.getEmployeeRequests with callbacks (snapshot) => { // ... }. Those callbacks will not be executed synchronously. They will be executed when the responses of the requests come back. So there are no managerRequests and employeeRequests when your setRequests([...managerRequests, ...employeeRequests]).
Here is a simple example solution
const MyComponent = () => {
const [state1, setState1] = React.useState([])
const [state2, setState2] = React.useState([])
React.useEffect(() => {
// Do the first subscribe
// state1 will be updated in the subscription's callback
}, [
// Dependencies of this effect
])
React.useEffect(() => {
// Do the second subscribe
// state2 will be updated in the subscription's callback
}, [
// Dependencies of this effect
])
React.useEffect(() => {
// Merge state1 and state2 here
}, [
state1,
state2
])
}
---- updated#20211203 ----
setState is asynchronous. You can checkout https://reactjs.org/docs/react-component.html#setstate for more information.
useEffect(() => {
// for example
console.log(state1) // [1, 2, 3]
console.log(state2) // [7, 8, 9]
setRequests([...state1, ...state2])
console.log(requests) // this is not guaranteed to be [1, 2, 3, 7, 8, 9]
}. [state1, state2])
If you want to do something based on the merged requests in the same component. You can write another useEffect:
useEffect(() => {
console.log(requests) // [1, 2, 3, 7, 8, 9]
}, [requests])

converting class to hooks getting Property 'then' does not exist on type '(dispatch: any) => Promise<void>'.ts(2339)

I'm new to react, here I have two same codes, one is with classes that work, and another is converted from that same class into hooks.
in hooks version, my 'then' is giving an error
Property 'then' does not exist on type '(dispatch: any) =>
Promise'.ts(2339)
have I made some mistake with conversion?
why it is not giving the same error in class while both are the same?
also console.log("Fetched model", realGraph.model); should give an object but it is giving undefined(in-class version it works), but if I put this console outside of loadGraph function then it gives an object, why it's not giving an object inside loadGraph function?
any ideas and suggestions?
class:
import { getGraph, getFloorplan, changeActiveCamera } from '../redux/actions';
const mapStateToProps = (state) => {
return {
currentSite: state.selection.currentSite,
currentCamera: state.selection.currentCamera,
};
};
function mapDispatchToProps(dispatch) {
return {
getGraph: (site) => dispatch(getGraph(site)),
getFloorplan: (site) => dispatch(getFloorplan(site)),
changeActiveCamera: (site, id) => dispatch(changeActiveCamera(site, id)),
};
}
loadGraph() {
if (this.props.currentSite) {
this.props.getFloorplan(this.props.currentSite.identif).then(() => {
console.log('Fetched floorplan');
this.props.getGraph(this.props.currentSite.identif).then(() => {
console.log('Fetched model', this.props.realGraph.model);
// new camera-related node & link status
if (this.props.currentCamera) {
this.props.changeActiveCamera(
this.props.currentSite.identif,
this.props.currentCamera.identif
);
}
});
});
}
}
converted from class to hooks:
Hooks:
const dispatch = useDispatch();
const realGraph = useSelector((state) => state.graphArticles.graph);
const currentSite = useSelector((state) => state.selection.currentSite);
const currentCamera = useSelector((state) => state.selection.currentCamera);
const dispatchGetFloorplan = (site) => dispatch(getFloorplan(site));
const dispatchGetGraph = (site) => dispatch(getGraph(site));
const dispatchChangeActiveCamera = (site, id) =>
dispatch(changeActiveCamera(site, id));
const loadGraph = () => {
if (currentSite) {
dispatchGetFloorplan(currentSite.identif).then(() => {
console.log('Fetched floorplan');
dispatchGetGraph(currentSite.identif).then(() => {
console.log('Fetched model', realGraph.model);
// new camera-related node & link status
if (currentCamera) {
dispatchChangeActiveCamera(
currentSite.identif,
currentCamera.identif
);
}
});
});
}
};
my action related to those:
export function getGraph(site) {
return getData(`api/graph/${site}`, GET_GRAPHS);
}
export function getFloorplan(site) {
return getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
}
On first glance, there are several things I would change in the code you provided.
First, don't use any wrapper factories over your dispatch functions. Use dispatch(action()) directly where you need it component. You aren't gaining anything by creating wrapper functions.
Second, it would be advisable to use some sort of middleware, like Redux Thunk, to handle async Redux actions (like fetching something from the API).
The actions you provided are just "dumb" functions, which are not returning promises so you can't expect it to be "then"-able in your target component.
I also advise the async/await syntax since it is much more readable.
Third, you need to leverage the Hooks reactive API with the useEffect hook.
So first try to define getFloorPlan and getGraph as async actions using the redux-thunk syntax.
export const getGraphAsync = (site) => async (dispatch) => {
try {
const data = await getData(`api/graph/${site}`, GET_GRAPHS);
dispatch(saveGraphData(data)) // save data into Redux store with a normal, synchronous action (plain object)
} catch (error) {
console.log(error)
}
}
export const getFloorplanAsync = (site) => async (dispatch) => {
try {
const data = await getImage(`api/graph/${site}/floorplan`, GET_FLOORPLAN);
dispatch(saveImageData(data)) // save data into Redux store with a normal, synchronous action (plain object)
} catch (error) {
console.log(error)
}
}
I am making an assumption that you correctly configured your store.js to use the thunk middleware.
And then refactor the rest of the component (following some best practices):
const someHookComponent = () => {
// ...
const dispatch = useDispatch();
const currentSite = useSelector((state) =>
state.selection.currentSite);
const currentCamera = useSelector((state) =>
state.selection.currentCamera);
const loadGraph = async () => {
if (currentSite) {
await dispatch(getFloorPlanAsync(currentSite.identif));
console.log('Fetched floorplan');
await dispatch(getGraphAsync(currentSite.identif));
console.log('Fetched model', realGraph.model); /* where is
realGraph coming from? */
/* Why is it important that these 2 dispatches follow one
another when there is no data being passed from one to the
other, or being used later in the component... */
});
});
}
};
useEffect(() => {
// new camera-related node & link status
if (currentCamera) {
dispatch(changeActiveCamera(
currentSite.identif,
currentCamera.identif
));
}
}, [currentSite?.identif, currentCamera?.identif]) /* null chaining is optional here */
// ...
}
I am guessing that loadGraph gets called by some onClick event somewhere down the line like this:
onClick={loadGraph}
If it is called inside useEffect, define the deps (variables used inside loadGraph):
useDeepCompareEffect(() => {
// ... some logic
loadGraph()
}, [currentSite, realGraph])
If you put your currentSite and currentCamera objects directly into the useEffect list of deps then you need to do a deep comparison "by hand".
In that case it's best to create a custom hook like useDeepCompareEffect which will do the heavy lifting of running deep comparisons of reference types under the hood (with the help of some library like lodash for example).
If you want to use or console.log the latest value of realGraph (reference type), you need to use the useEffect hook with a deep comparison again (or just extract the target primitive directly into the deps list and use vanilla useEffect) :
useDeepCompareEffect(() => {
if (realGraph) {
console.log('Fetched model', realGraph.model);
}
}, [realGraph]) // reference type
// or
useEffect(() => {
if (realGraph) {
console.log('Fetched model', realGraph.model);
}
}, [realGraph.someProperty]) // primitive

How to add new objects to an existing useState array using map() or foreach()?

I'm trying to create a Twitter clone, and I'm having troubles with my news feed. Basically it pulls tweets from my firebase database for each user followed by the current user. So say you follow Jon Abrahams and Terry Crews, for each of these 2, it'll pull the "tweets" collection, and for each tweet, it'll return the data.
I did this with useState and useContext ( since I needed Context and couldn't make it work in a class component, but also needed state ).
const CurrentUser = useContext(CurrentUserContext);
const [tweets, setTweets] = useState({tweets: []});
const feedTheFeed = async (id) => {
const followedUsers = await getFollowedUsers(id);
if(followedUsers) {
followedUsers.docs.forEach(async doc => {
const followedId = doc.data();
const unformatTweets = await getTweetsByUser(followedId.follows);
if(unformatTweets.docs) {
unformatTweets.docs.map(unformatTweet => {
const tweetText = unformatTweet.data();
setTweets({
tweets: [...tweets, tweetText]
})
// console.log(tweetText);
})
}
})
// console.log(tweets);
}
}
useEffect(() => {
if(!CurrentUser) return;
if(CurrentUser.id || CurrentUser.uid) {
feedTheFeed(CurrentUser.id);
}
}, [CurrentUser]);
The problem is that there's an issue when loading the component, it says that "tweets is not iterable", but it's an array, so I don't see why it wouldn't work. Does anyone have an idea ?
Thank you !
Seems like what you want is
const [tweets, setTweets] = useState([]);
and
setTweets([...tweets, tweetText])
I think what you want to do is this.....
const [tweets, setTweets] = useState([]);
setTweets(b => [...b, tweetText])

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

Categories