Infinite loop when working with react and react-firebase-hooks

Viewed 845

I am working with a navigation bar that should be able to switch between multiple chat rooms using react and react-firebase-hooks. (https://github.com/CSFrequency/react-firebase-hooks)

However, the chat room will infinitely re-render itself when I choose a room in nav-bar.

I initially thought this is a router issue, but having each rooms sharing the same url, the issue persists.

Right now, when I choose a room using the nav bar, it will send that room number back to App.js using a callback function. App.js will pass that room number to ChatRoom.js, which will get the data from firestore, and re-render itself.

I struggled for several days trying to find anything that could cause the infinite loop with minimal success. Any help would be appreciated!

ChatRoom.js

import React, { useMemo, useRef, useState } from 'react';
import { withRouter } from 'react-router';
import { useCollectionData, useDocument, useDocumentData } from 'react-firebase-hooks/firestore';
import firebase, { firestore, auth } from '../Firebase.js';
import ChatMessage from './ChatMessage';


const ChatRoom2 = (props) => {

    console.log("chat room rendered");
    
    function saveQuery(){
        const channelid= props.channelid;
        const messagesRef = firestore.collection('messages').doc(channelid).collection('chats');
        const query = messagesRef.orderBy('createdAt').limitToLast(25);
        return [messagesRef,query];
    }

    var returnedVal = useMemo(()=>saveQuery , [props.channelid]);
    const messagesRef = returnedVal[0];
    const query = returnedVal[1];

    const [messages] = useCollectionData(query, { idField: 'id' });
    const [formValue, setFormValue] = useState('');
    
    const sendMessage = async (e) => {
        e.preventDefault();

        console.log(messagesRef);
        console.log(query);
        console.log(messages);

        const { uid, photoURL } = auth.currentUser;
        
        await messagesRef.add({
            text: formValue,
            createdAt: firebase.firestore.FieldValue.serverTimestamp(),
            uid,
            photoURL
        })

        setFormValue('');
    }
    return (<>
        <main>
            {messages && messages.map(msg => <ChatMessage key={msg.id} message={msg} />)}
        </main>

        <form onSubmit={sendMessage}>

            <input value={formValue} onChange={(e) => setFormValue(e.target.value)} placeholder="say something nice" />

            <button type="submit" disabled={!formValue}>️</button>

        </form>
    </>)
}


export default ChatRoom2;

ChatList.js (nav bar)

const ChatList = (props) => {

    console.log("list rendered");
    const query = firestore.collection('users').doc(auth.currentUser.uid).collection('strangers').orderBy('channelID').limitToLast(10);
    //console.log(query);
    const [channelidArr] = useCollectionData(query, { idField: 'id' });

    return (
        <div>
            {channelidArr && channelidArr.map(channelid =>
                <div>
                    <button onClick={() => props.parentCallback(channelid.channelID)}>{channelid.channelID}</button>
                    
                    <br />
                </div>)}

        </div>

    );
};

export default ChatList;

App.js

import React, { useRef, useState } from 'react';
import {
  BrowserRouter,
  Switch,
  Route,
  Link
} from "react-router-dom";

//import './App.css';

import firebase, { firestore, auth } from './Firebase.js';
import { useAuthState } from 'react-firebase-hooks/auth';
import { useCollectionData } from 'react-firebase-hooks/firestore';

import ChatList from './components/ChatList.js';
import FindNew from './components/FindNew.js';
import Footer from './components/Footer.js';
import Profile from './components/Profile.js';
import ChatRoom2 from './components/ChatRoom2.js';
import SignOut from './components/SignOut.js';
import SignIn from './components/SignIn.js';
import SignUp from './components/SignUp.js';
import ChatRoom from './components/ChatRoom.js';


function App() {
  console.log('App rendered');
  const [user] = useAuthState(auth);
  const [roomNum, setRoomNum] = useState([]);

  const callbackFunction = (childData) => {
      setRoomNum(childData);
  };
  return (
    <div className="App">
      <header>
        <h1>⚛️</h1>
        <SignOut auth={auth} />
      </header>

      <BrowserRouter >
        <Footer />
        <Switch>
          <Route path="/profile">
            <Profile />
          </Route>
          <Route path="/new">
            <FindNew />
          </Route>
          
          <Route path="/signup">
            {() => {
              if (!user) {
                return <SignUp />;
              } else {
                return null;
              }
            }}
          </Route>
          
          <Route path="/direct">
            {user ?
              <div>
                <ChatList parentCallback={callbackFunction} />
                <ChatRoom2 channelid={roomNum} />
              </div> : <SignIn />}
          </Route>
        

        </Switch>

      </BrowserRouter>

    </div>
  );
};

export default App;

1 Answers

Issue Summary

useCollectionData memoizes on the query parameter but since a new query reference was declared each render cycle the firebase hook was rerun and updated collection and rerendered the component.

const { channelid } = props;
const messagesRef = firestore
  .collection('messages')
  .doc(channelid)
  .collection('chats');
const query = messagesRef // <-- new query reference
  .orderBy('createdAt')
  .limitToLast(25);

const [messages] = useCollectionData(
  query, // <-- reference update trigger hook
  { idField: 'id' },
);

Solution

query has only a dependency on the channelid prop value, so we can memoize the query value and pass a stable value reference to the useCollectionData hook.

const { channelid } = props;

const query = useMemo(() => {
  const messagesRef = firestore
    .collection('messages')
    .doc(channelid)
    .collection('chats');
  const query = messagesRef.orderBy('createdAt').limitToLast(25);
  return query;
}, [channelid]);

const [messages] = useCollectionData(
  query,  // <-- stable reference
  { idField: 'id' },
);
Related