0

Edit: I don't understand the reason for downvotes, this was a good question and no other questions on this site solved my issue. I simply preloaded the data to solve my issue but that still doesn't solve the problem without using functional components.

I'm trying to pass users last message into the ListItem subtitle prop but I can't seem to find a way to return the value from the promise/then call. It's returning a promise instead of the value which gives me a "failed prop type". I thought about using a state but then I don't think I could call the function inside the ListItem component anymore.

  getMsg = id => {
    const m = fireStoreDB
      .getUserLastMessage(fireStoreDB.getUID, id)
      .then(msg => {
        return msg;
      });
    return m;
  };

  renderItem = ({ item }) => (
    <ListItem
      onPress={() => {
        this.props.navigation.navigate('Chat', {
          userTo: item.id,
          UserToUsername: item.username
        });
      }}
      title={item.username}
      subtitle={this.getMsg(item.id)} // failed prop type
      bottomDivider
      chevron
    />
  );
Wes
  • 1,847
  • 1
  • 16
  • 30
  • Does this answer your question? [How do I return the response from an asynchronous call?](https://stackoverflow.com/questions/14220321/how-do-i-return-the-response-from-an-asynchronous-call) – HMR Nov 08 '19 at 08:38
  • @HMR - I think it's slightly more specific than that. – T.J. Crowder Nov 08 '19 at 08:39
  • You can use [setState](https://www.valentinog.com/blog/await-react/) in the resolve. – HMR Nov 08 '19 at 08:46
  • [The following answer](https://stackoverflow.com/a/58763566/1641941) comes close to what you can do, if you can't use functional components then the link I posted would give you an idea how to solve this with class components (only check the code, you don't need to configure babel) – HMR Nov 08 '19 at 09:46
  • @HMR It doesn't but I did ended up fixing the problem. – Wes Nov 08 '19 at 11:48
  • If you don't want to use functional components, see my answer below, making the network call in the ListView's `componentDidMount()` callback instead of by `eseEffect()`, and using traditional `setState` to store the returned value. – tobiasfried Nov 08 '19 at 19:51

3 Answers3

4

You could only do it that way if ListItem expected to see a promise for its subtitle property, which I'm guessing it doesn't. ;-) (Guessing because I haven't played with React Native yet. React, but not React Native.)

Instead, the component will need to have two states:

  • The subtitle isn't loaded yet
  • The subtitle is loaded

...and render each of those states. If you don't want the component to have state, then you need to handle the async query in the parent component and only render this component when you have the information it needs.

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
0

If the 'last message' is something specific to only the ListItem component and not something you have on hand already, you might want to let the list item make the network request on its own. I would move the function inside ListItem. You'll need to set up some state to hold this value and possibly do some conditional rendering. Then you'll need to call this function when the component is mounted. I'm assuming you're using functional components, so useEffect() should help you out here:

//put this is a library of custom hooks you may want to use
//  this in other places
const useIsMounted = () => {
  const isMounted = useRef(false);
  useEffect(() => {
    isMounted.current = true;
    return () => (isMounted.current = false);
  }, []);
  return isMounted;
};

const ListItem = ({
  title,
  bottomDivider,
  chevron,
  onPress,
  id, //hae to pass id to ListItem
}) => {
  const [lastMessage, setLastMessage] = useState(null);
  const isMounted = useIsMounted();
  React.useEffect(() => {
    async function get() {
      const m = await fireStoreDB.getUserLastMessage(
        fireStoreDB.getUID,
        id
      );
      //before setting state check if component is still mounted
      if (isMounted.current) {
        setLastMessage(m);
      }
    }
    get();
  }, [id, isMounted]);

  return lastMessage ? <Text>DO SOMETHING</Text> : null;
};
HMR
  • 37,593
  • 24
  • 91
  • 160
tobiasfried
  • 1,689
  • 8
  • 19
  • You forgot async: `useEffect(async () => {` but you could just as well do `fireStoreDB.get().then(setLastMessage)` and leave out async and await. – HMR Nov 08 '19 at 09:23
  • Whoops, you're right. Still, though it's only syntactic sugar, it's a good visual marker when you're scanning code quickly, and it's more readable. – tobiasfried Nov 08 '19 at 09:27
  • Your answer comes close but should check if component is [still mounted](https://github.com/jmlweb/isMounted) before setting state. The effect has missing dependencies (id) and effect should not be an async function (linter will complain). I can adjust your answer if you want. – HMR Nov 08 '19 at 09:38
  • Sure, I'm on my phone atm – tobiasfried Nov 08 '19 at 09:41
  • I updated your answer, no error handling or loading message but comes close. – HMR Nov 08 '19 at 09:44
0

I fixed the issue by using that promise method inside another promise method that I had on componentDidMount and added user's last message as an extra field for all users. That way I have all users info in one state to populate the ListItem.

  componentDidMount() {
    fireStoreDB
      .getAllUsersExceptCurrent()
      .then(users =>
        Promise.all(
          users.map(({ id, username }) =>
            fireStoreDB
              .getUserLastMessage(fireStoreDB.getUID, id)
              .then(message => ({ id, username, message }))
          )
        )
      )
      .then(usersInfo => {
        this.setState({ usersInfo });
      });
  }

  renderItem = ({ item }) => (
    <ListItem
      onPress={() => {
        this.props.navigation.navigate('Chat', {
          userTo: item.id,
          UserToUsername: item.username
        });
      }}
      title={item.username}
      subtitle={item.message}
      bottomDivider
      chevron
    />
  );
Wes
  • 1,847
  • 1
  • 16
  • 30
  • You're mutating a value and setting the state with that, assuming getUserLastMessage will resolve in the same order as requestion (or order of the users in the list is not important) and needlessly set state many times. With Promise.all you can set state once, get the responses in the right order and don't need to mutate a temporary value. – HMR Nov 08 '19 at 12:30
  • @HMR I'm using the value from `getAllUsersExceptCurrent` for `getUserLastMessage` so I won't be able to wrap them both in a `Promise.all`. However, I did fix the "setting state multiple times" by using a normal for loop and do a check for when it's the last item to do a set state. – Wes Nov 08 '19 at 21:10
  • I think something like this should work: `Promise.all( users.map( ({id,username})=>fireStoreDB .getUserLastMessage(fireStoreDB.getUID, id) .then(message=>({message,id,username})) ) ).then(userInfo=>this.setState({userInfo}))` – HMR Nov 08 '19 at 21:32
  • @HMR There's a syntax error on '=>' in between users.map and fireStoreDB.getUserLastMessage which I don't know how to fix but I fixed some of the others by doing `Promise.all([users.map(({id, username})) => fireStoreDB.getUserLastMessage(fireStoreDB.getUID, id).then(message=> ({message, id, username})).then(usersInfo => this.setState(usersInfo))])` – Wes Nov 08 '19 at 22:41
  • 2
    CLUTCHER, @HMR is correct that the version in the question is likely unreliable (if your last request for message completes before one of the previous ones, you set state prematurely). Here's how to do it with `Promise.all`: https://pastebin.com/VmMqs9i2 – T.J. Crowder Nov 09 '19 at 09:40
  • 1
    Here's that code inline, but code in comments is ugly. :-) ```componentDidMount() { // Get the users fireStoreDB.getAllUsersExceptCurrent() .then( // Map them to objects including last message users => Promise.all(users.map( // Uses destructuring on the user to pick out `id` and `username` ({id, username}) => fireStoreDB .getUserLastMessage(fireStoreDB.getUID, user.id) // Builds and returns object from `id`, `username`, and `message` .then(message => ({id, username, message})) ) ) ) .then(usersInfo => {``` – T.J. Crowder Nov 09 '19 at 09:41
  • 1
    ```// Now that we have all the `usersInfo` entries, set state this.setState({usersInfo}); }); }``` – T.J. Crowder Nov 09 '19 at 09:41
  • @T.J.Crowder On getUsersLastMessage, `user.id` was undefined but I replaced it with `id` and everything worked, thanks :) – Wes Nov 10 '19 at 23:14