1

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

Ryan Sam
  • 2,858
  • 4
  • 19
  • 30

3 Answers3

1

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 />);
}
goto
  • 4,336
  • 15
  • 20
0

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

sojin
  • 2,158
  • 1
  • 10
  • 18
0

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/>}
Maruth51
  • 36
  • 8