Infinite loop in React Native because of a useState

Viewed 603

I've got 1/ an infinite loop using a useState to save a data from an axios request. I want to inject the value in a view during a mapping :

import { StyleSheet, Text, View, TextInput, FlatList, Button, ScrollView } from 'react-native';
import styles from './styles'
import React, { useEffect, useState, Component } from 'react';
import App, { currentCityCode } from './app'
import axios from 'axios';
import { Dropdown } from 'react-native-material-dropdown';

function Chat(props) {
    useEffect(() => {
        getMessage()
    }, []
    );

    const messApiRest = "https://..."
    const [lastMessage, setLastMessage] = useState("")
    const [user, setUser] = useState("")
    const [categoryMessage, setcategoryMessage] = useState("")
    const [idUser, setIdUser] = useState("")
    const [allMessages, setAllMessages] = useState([])
    const [messageOneByOne, seMessageOneByOne] = useState([])

    function getMessage() {
        axios.get(messApiRest + "/api/messages",
            {
                headers: {
                    'Authorization': "Bearer " + props.token,
                },
                params: {
                    'citycode': props.cityCode
                },
            })
            .then(response => {
                setAllMessages(response.data.results)

            },
            )
    }

    function getUserName(toto) {

        axios.get(messApiRest + "/api/users/" + toto,
            {
                headers: {
                    'Authorization': "Bearer " + props.token,
                }
            }
        )
            .then(response => {
                console.log(response.data.username)
                setUser(response.data.username)
            })
    }

    return (
        <View style={styles.container}>

            {allMessages.map(function (message) {
                getUserName(message.author.id)

                return (
                    <Text>{user}: {message.content}</Text>
                )
            })}

        </View>
    )
}

export default Chat

The main problem is that if a save my {user} using a useState, i create an infinite loop.

2/ I've tried avoiding that by not saving it with the useState, and directly return the value response.data.username in the view, but it doesn't work. I know I can't return the value of a promise, that's why I'm using a useState instead.

I'm a begginer {probably not a good one}, I'm trying to understand how React and axios works, but i'm at my limit and can't guess a solution...

Thank you for your help, if someone dare ^^

3 Answers

The central problem is here is that every render calls getUserName, which in turn sets state, which triggers another render.

render => getUserName => setUser => render => getUserName => setUser => render

One solution would be creating a separate username component that handles the name resolution:

const DisplayName = ({userid}) => {
    const [displayName, setDisplayName] = useState();
    useEffect(() => {
      // resolve username and call setDisplayName
    }, [userid]);

    return (
      <span>{displayName}</span>
    );
}

Then you can pass the author id and let it handle it:

return (
  <View style={styles.container}>
    {allMessages.map(message => (
        <Text>
          <DisplayName userid={message.author.id} />: {message.content}
        </Text>
      ))
    }
  </View>
)

You still have the issue of re-fetching the username for every message in the chat, but it solves the infinite loop issue.

To solve the repetitive username fetching, you could either cache them somewhere outside the component (a store, a la redux) or move it back to the parent component and resolve all usernames up front:

const [usernames, setUsernames] = useState({});

const resolveUsernames = useCallback(async authorIds => {
  // create an array of 'getUserName' promises and wait for them to all come back
  const usernames = await Promise.all(authorIds.map(id => getUserName(id)));

  // map the names back to the corresponding ids, e.g. { 123: 'alice', 456: 'bob'}
  // Promise.all results come back in the same order as the input promises, so you can associate them by index.
  const idMap = authorIds.reduce((ids, id, index) => ({
    ...ids,
    [id]: usernames[index]
  }), {});

  // store the id-to-name mapping in state:
  setUsernames(idMap);
}, [authorIds])


function getMessage() {
  axios.get(messApiRest + "/api/messages",
    {
      headers: { 'Authorization': "Bearer " + props.token },
      params: {  'citycode': props.cityCode },
    })
    .then(response => {
      const results = response.data.results;
      // this is just a shorthand way of getting an array of distinct ids
      const authorIds = [...new Set(results.map(message => message.author.id))];
      resolveUsernames(authorIds);
      setAllMessages(response.data.results)
    })
}

With that in place, you can look up the username for each message from the component's usernames state:

{allMessages.map(message => (
  <Text>
    {usernames[message.author.id]}: {message.content}
  </Text>
))
}

The infinite loop is because of calling getUserName in the returned JSX.

You can try fetching the users and messages inside useEffect and only update the state when both of them are fetched:

function Chat(props) {
  const messApiRest = "https://...";
  const [lastMessage, setLastMessage] = useState("");
  const [user, setUser] = useState("");
  const [categoryMessage, setcategoryMessage] = useState("");
  const [idUser, setIdUser] = useState("");
  const [allMessages, setAllMessages] = useState([]);
  const [messageOneByOne, seMessageOneByOne] = useState([]);

  useEffect(() => {
    getMessage()
      .then((res) => {
        return res.data.results;
      })
      .then((msgs) => {
        const userPromises = msgs.map((m) => getUserName(m.author.id));
        Promise.all(userPromises).then((users) => {
          setAllMessages(
            msgs.map((msg, index) => ({ ...msg, user: users[index] }))
          );
        });
      });
  }, []);

  function getMessage() {
    return axios.get(messApiRest + "/api/messages", {
      headers: {
        Authorization: "Bearer " + props.token,
      },
      params: {
        citycode: props.cityCode,
      },
    });
  }

  function getUserName(toto) {
    return axios.get(messApiRest + "/api/users/" + toto, {
      headers: {
        Authorization: "Bearer " + props.token,
      },
    });
  }

  return (
    <View style={styles.container}>
      {allMessages.map(function (message) {
        return (
          <Text>
            {message.user}: {message.content}
          </Text>
        );
      })}
    </View>
  );
}

export default Chat;

In this example I merged user and messages into allMessages but you can change it as you like.

Thank you for your answer. I've tried both, every solved some problems but not all.

I might have been a bit more specific : I'm looking for a more simple code, even if I have to re-think the logic of my approach.

Here is what I want to do :

1/ Acces an API with axios to get the datas of all messages posted in a certain area. Theses datas gives me the content of the message, witch I want to display, and the Id. of the user.

2/ I want to use the Id. of the user to make a second call to the API, in order to get this time the name of the user, to display it before the content of the message.

The main approches I chose : A/ I can save both content of messages and username in different arrays and map them in parallel to display datas. B/ I can only call an API to get all the datas of messages, stock them in an array, then map the array to display the content of the message, and make at that time the second call to the API to retrieve the name of the user from his Id. That's the solution I tried.

Yeah, I'm a noob, but I think what I want to do is basic and don't need that many lines of code. But maybe I'm wrong ^^

Related