2

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;

9
  • Can you tell which component is render looping? Have you tried simplifying your code even more to isolate which component/hook usage is triggering the render looping? Commented Jan 24, 2021 at 7:15
  • Hi, thanks for your reply. the chat room component is causing the infinite loop, the sequence of event happened is: App render -> chat list renders -> chat room renders ->chat list renders -> chat room renders infinitely. In the chunk.js file, it says " const { channelid } = props.channelid; " is causing the error. I'm new with react and I'm sorry if certain things i say are not making any sense. Commented Jan 24, 2021 at 18:59
  • 1
    It's probably difficult to create a running codesandbox with this code (you could try though). Few thoughts. (1) What if you used the useCollectionDataOnce hook in the chatroom component? (2) If you comment out the const [messages] = useCollectionData(query, { idField: 'id' }); in chatroom does it still render-loop? Can you include a more complete app.js component so we can see how the callback and roomNum value is maintained. Commented Jan 24, 2021 at 19:26
  • Hi, thank you for replying again! I tried your suggestions.1) using useCollectionDataOnce hook in chatroom also causes infinite loop. 2) removing useCollectionData does not cause infinite loop. 3) I updated App.js which now includes the callback function and roomNum state. Commented Jan 25, 2021 at 1:29
  • Thanks. Ok, I suspect it could be caused by query being redeclared each render cycle and that query ends up being a useCollectionData hook dependency (I've dug through the source code and I think this is the case). Try using a useMemo hook to memoize the query value with a dependency on the channelid prop so query is a stable reference value for the useCollectionData hook. Commented Jan 25, 2021 at 16:46

1 Answer 1

4

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' },
);
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.