SetState causes many rerenders

Viewed 385

I am creating a messenger app and I have a component for the main screen of all the messages. I'm trying to ensure every time a user sends or receives a message, the component renders and updates the Flatlist with the last sent message. I have a listener to firebase realtime database to listen for updates. I’m using functional components in my app and I think I’m using setState incorrectly because it causes my component to rerender many times. I have thoroughly checked the asked questions and none of them seem to relate to my issue. Here's my code.


const AllMessagesScreen = ({ navigation }) => {
    const uid = firebase.auth().currentUser.uid;

    const [state, setState] = useState({
        allMessages: []
    });

    const { allMessages } = state;

    const fsdb = firebase.firestore().collection('Users');
    const db = database().ref(`messages/${uid}`); //realtime database

    const get = async () => {

        try {
            await db.on('value', snapshot => {

                snapshot.forEach(item => {

                    const { key: _id } = item;
                    const { lastMessage } = item.val();

                    fsdb.doc(_id).get().then((doc => {
                        const otherUser = doc.data();
                        const newChat = {
                            id: _id,
                            name: otherUser.name,
                            picture: otherUser.pictures.thumbnailUrl,
                            lastMessage: lastMessage
                        };
                        setState(prevState => ({
                             allMessages: [...prevState.allMessages, newChat]
                        }));
                    })).catch((err) => {
                        console.log('caught', err)
                    });
                });
            });
        } catch (err) {
            console.log('error caught', err)
        };

    };

    useEffect(() => {
        get();

    }, []);

    return (
        <View style={styles.screen}>
            <FlatList
                data={allMessages}
                keyExtractor={item => item.id}
                renderItem={({ item }) => (
                    <View style={{ flex: 1 }}>
                        <MessageCard
                            id={item.id}
                            name={item.name}
                            image={item.picture}
                            lastMessageText={item.lastMessage.text}
                        />
                    </View>
                )}
            />

        </View>
    );

}

I have also tried setting the state this way

setState({ allMessages: [...allMessages, newUser] });
``
The second way only shows the last rendered message and gets rid of the rest.
Any help is really appreciated
1 Answers

You should not update state in a loop, take out the setState from inside the loop and call it after the loop.

On each state update React will re-render the component, in this case React will re-render the component after each iteration, Which can cause unexpected result.

You have couple of other issue as well that I would like to highlight.

  1. You should not use state like const { allMessages } = state; it will always give you an empty array on each render. use it like state.allMessages whenever is needed.

  2. On each rendr you are getting the database connection which can cause memory leak, use useRef to hold the db connection.

  3. Initialized the db connection and other stuff inside the useEffect hook (on component mount).

I have updated the code which will solve the above issues (I have changed the then block with await as you are you async/await in your code and replaced forEach with for of).

import React, { useState, useRef } from 'react';
const AllMessagesScreen = ({ navigation }) => {
    const [state, setState] = useState({
        allMessages: []
    });
    let fsdb = useRef();
    let db = useRef();
    useEffect(() => {
        const uid = firebase.auth().currentUser.uid;
        fsdb.current = firebase.firestore().collection('Users');
        db.current = database().ref(`messages/${uid}`); //realtime database
        fetchMessage();
    }, []);
    async function fetchMessage() {
        try {
            await db.current.on('value', snapshot => {
                let newChat = []
                for (const item of snapshot) {
                    const { key: _id } = item;
                    const { lastMessage } = item.val();
                    let doc = await fsdb.current.doc(_id).get();
                    const otherUser = doc.data();
                    newChat.push({
                        id: _id,
                        name: otherUser.name,
                        picture: otherUser.pictures.thumbnailUrl,
                        lastMessage: lastMessage
                    });
                }
                setState(prevState => ({
                    allMessages: [...prevState.allMessages, ...newChat]
                }));
            });
        } catch (err) {
            console.log('error caught', err)
        };
    };

    return (
        <View style={styles.screen}>
            <FlatList
                data={state.allMessages}
                keyExtractor={item => item.id}
                renderItem={({ item }) => (
                    <View style={{ flex: 1 }}>
                        <MessageCard
                            id={item.id}
                            name={item.name}
                            image={item.picture}
                            lastMessageText={item.lastMessage.text}
                        />
                    </View>
                )}
            />
        </View>
    );
}
Related