简体   繁体   English

嵌套异步 function 中的 setState - React Hooks

[英]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?如何构建一个 function 异步获取一些数据然后使用该数据获取更多异步数据?

I am using Dexie.js (indexedDB wrapper) to store data about a direct message.我正在使用 Dexie.js(indexedDB 包装器)来存储有关直接消息的数据。 One thing I store in the object is the user id which I'm going to be sending messages to.我存储在 object 中的一件事是我将要向其发送消息的用户 ID。 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.为了构建更好的 UI,我还获取了有关该用户的一些信息,例如存储在远程 rdbms 上的个人资料图片、用户名和显示名称。 To build a complete link component in need data from both databases (local indexedDB and remote rdbms).要构建一个完整的链接组件,需要来自两个数据库(本地 indexedDB 和远程 rdbms)的数据。

My solution returns an empty array.我的解决方案返回一个空数组。 It is being computed when logging it in Google Chrome and I do see my data.在 Google Chrome 中记录它时正在计算它,我确实看到了我的数据。 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我的计划是使用 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:当您尝试执行以下操作时,我相信您的问题与您的第二个useEffect挂钩有关:

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 .不幸的是,这不起作用,因为async/await不适用于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.您需要使用for...ofPromise.all()正确遍历所有conversations ,调用您的API ,然后设置state完成后。

Here's is a solution using Promise.all() :这是使用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:或者,使用for...of ,您可以执行以下操作:

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 />);
}

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 foreach 也不会像您期望的那样等待使用 for 循环而不是 foreach

I am not sure it is solution for your question but it could help you to solve your error我不确定它是否可以解决您的问题,但它可以帮助您解决错误

i think it is happening because for getReceivers() function is asynchronous.我认为它正在发生,因为对于 getReceivers() function 是异步的。 it waits for the response, in that meantime your state renders with empty array.它等待响应,同时您的 state 以空数组呈现。

you can display spinner untill the response received.您可以显示微调器,直到收到响应。 like喜欢

const[isLoading,setLoading]= useState(true)
    useEffect(()=>{
         getReceivers().then(()=>{setLoading(false)}).catch(..)
        } )
  return  {isLoading ? <spinner/> : <yourdata/>}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM