Here I am using react-router and using conditionals for multiple routes. I am using context to store my state. I want to load my data from localStorage to the state before my App.js is loaded as based on that it tells on which routes should I navigate to.
userContext.js
import React, { useState, createContext, useEffect } from "react";
export const userContext = createContext();
export const UserProvider = (props) => {
const [user, setUser] = useState(null);
const loadFromLocalStorage = () => {
try {
const serializedState = localStorage.getItem("state");
if (serializedState == null) return undefined;
return JSON.parse(serializedState);
} catch (err) {
console.log(err);
return null;
}
};
useEffect(() => {
const state = loadFromLocalStorage();
console.log("state: ", state);
setUser(state);
}, []);
return (
<userContext.Provider value={[user, setUser]}>
{props.children}
</userContext.Provider>
);
};
export default userContext;
App.js
import React, { useContext, useEffect } from "react";
import { Switch, Route, Redirect } from "react-router";
import userContext from "./context/userContext";
const App = () => {
var routes = null;
const [user, setUser] = useContext(userContext);
console.log("app: ", user);
console.log(routes);
if (user == null) {
routes = (
<div>
<Switch>
<Route path="/login" component={SignUp} />
<Redirect to="/login"></Redirect>
</Switch>
</div>
);
} else {
routes = (
<div>
<Switch>
<Route exact path="/news" component={NewsList} />
<Redirect to="/news"></Redirect>
</Switch>
</div>
);
}
return <div className="App">{routes}</div>;
};
export default App;
Inside your UserProvider, don't render the children until you've loaded data from localstorage. This will require some extra state, but nothing too bad, and this is a common pattern for asynchronous context/state:
export const UserProvider = (props) => {
const [isReady, setIsReady] = useState(false);
// other code removed for brevity
useEffect(() => {
const state = loadFromLocalStorage();
setIsReady(true);
}, [])
return (
<userContext.Provider value={[user, setUser]}>
{isReady ? props.children : null}
</userContext.Provider>
);
}
Related
I tried making a global state by using a Context and have the following for my AuthProvider.js file
import { createContext, useState } from "react";
const AuthContext = createContext({});
export const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({});
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
)
}
export default AuthContext;
I want to be able to change its boolean state within an if-else instead of a button with an onCLick event in the tutorial but it shows an error in the console: setAuth is not a function
function Home() {
const setAuth = useContext(AuthProvider);
const [usernameReg, setUsernameReg] = useState("");
const [passwordReg, setPasswordReg] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [loginStatus, setloginStatus] = useState("");
const uLogin = document.getElementById('userLogin');
const pLogin = document.getElementById('passwordLogin');
const register = () => {
if (usernameReg === "" || passwordReg === ""){
alert("Fields can't be blank");
return;
}
Axios.post('http://localhost:3001/register', {
username: usernameReg,
password: passwordReg,
}).then((response) => {
console.log(response);
});
}
const login = () => {
Axios.post('http://localhost:3001/login', {
username: username,
password: password,
}).then((response) => {
if (response.data.message){
setloginStatus(response.data.message);
alert(response.data.message);
}
else{
setloginStatus(response.data[0].username);
setAuth(true); <====================================== here
window.location.href = "/map";
}
console.log(response.data);
});
}
return (
<div className="Home">
<h1>Register</h1>
<label>Username: </label>
<input type="text"
onChange={(e) => {
setUsernameReg(e.target.value)
}}/>
<label><p></p>Password: </label>
<input type="text"
onChange={(e)=> {
setPasswordReg(e.target.value)
}}/>
<p></p>
<button onClick={register}> Register </button>
<h1>--------------------------------------------------------</h1>
{/*<form id="loginForm">*/}
<h1>Log In</h1>
<input id="userLogin" type="text" placeholder="Username"
onChange={(e) => {
setUsername(e.target.value)
}}
/>
<p></p>
<input id="passwordLogin" type="password" placeholder="Password"
onChange={(e) => {
setPassword(e.target.value)
}}
/>
<p></p>
<button onClick={login}> Log In </button>
{/*</form>*/}
<h1>{loginStatus}</h1>
</div>
)
}
my App.js is below
import React, { useState } from "react";
import { BrowserRouter,Route, Routes} from 'react-router-dom';
import './App.css';
import Home from "./Pages/Home";
import MapPage from "./Pages/MapPage.js";
import ProtectedRoutes from "./ProtectedRoutes";
import { AuthProvider } from "./components/AuthProvider"
function App() {
const [auth, setAuth ] = useState(false);
//const [auth, setAuth] = useState(false);
return (
<BrowserRouter>
<Routes element ={<AuthProvider />} value={{auth, setAuth}}>
<Route element ={<ProtectedRoutes />}>
<Route element={<MapPage />} path="/map" exact/>
</Route>
<Route path="/" element={<Home />}/>
</Routes>
</BrowserRouter>
);
}
export default App;
index.js below
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { AuthProvider } from './components/AuthProvider'
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<AuthProvider>
<App />
</AuthProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bitly/CRA-vitals
reportWebVitals();
I'm very new to JavaScript and I've only been following along tutorials for the functions or features I need for the project. Is there a way for this?
I'm also guessing that the code for the context file is overkill for a simple Boolean state.
You probably want to check the context docs again, and try to understand how and why you set them up, and how to access them. You tried accessing the context by passing the provider as a param to the useContext hook. This is wrong, you need to pass the Context itself. You also called the return from the context setAuth, but the return is the context itself. I urge you to read the documentation thourougly. However, this is probably what you wanted:
type AuthContextType = {
auth: boolean;
setAuth: Dispatch<SetStateAction<boolean>>;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider = ({ children }: PropsWithChildren<{}>) => {
const [auth, setAuth] = useState(false);
return (
<AuthContext.Provider value={{ auth, setAuth }}>
{children}
</AuthContext.Provider>
);
};
const useAuthContext = () => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error("No AuthContext Provider found");
return ctx // as AuthContextType<...> if you need the context to be generic and to infer generics.
};
function Home() {
const { auth, setAuth } = useAuthContext();
...
}
I am having some issues with my routing currently when authenticated. Whenever I try to access my ViewPortfolio page at localhost:3000/portfolio/portfolioId it will redirect me back to my homepage. I am not sure what is going on. I have also tried manipulating the URL by modifying it to the correct URL link but it also redirects me back to /homepage when I am authenticated. The source codes can be found below. App.js is my router with PrivateRoute as the private route component and finally, CreateInterview.js where I redirect using js windows.location function to ViewPortfolio.js which will use useParams() react hook to get the param. But instead now after creating successfully and redirect to the correct URL with the portfolioId it will redirect back to homepage within less than a second.
PrivateRoute.js
import React from 'react'
import { Route, Redirect } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const PrivateRoute = ({ component: Component, ...rest }) => {
const { currentUser } = useAuth()
return (
<Route
{...rest}
render={(props) => {
if (currentUser) {
return <Component {...props} />
} else {
return <Redirect to={{
pathname: "/",
state:{
from: props.location
}
}}/>
}
}
}>
</Route>
)
}
export default PrivateRoute
App.js
import React from "react"
.
.
.
import PublicRoute from "./PublicRoute";
function App() {
return (
<AuthProvider>
<Router>
<Switch>
{/* Auth Routes */}
<PublicRoute exact path='/' component={Login} />
.
.
.
<PrivateRoute exact path='/createInterview' component={CreateInterview} />
<PrivateRoute path='/manageInterview' component={ManageInterview} />
<PrivateRoute path='/portfolio/:portfolioId' component={ViewPortfolio} />
{/* Non-Existance Routes */}
<Route path="*" component={() => "404 NOT FOUND"} />
</Switch>
</Router>
</AuthProvider>
)
}
export default App
CreatInterview.js redirecting in js (onSubmit of the form)
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
}).then(function(docRef) {
console.log("This is the Document ID " + docRef.id.toString());
console.log(docRef.id);
window.location = '/portfolio/' + docRef.id;
})
setLoading(false)
}
Part of ViewPortfolio.js to receive the portfolioId from CreateInterview.js
const ViewPortfolio = () => {
let { portfolioId } = useParams();
AuthContext.js
import React, { useContext, useState, useEffect } from "react"
import { auth, database } from "../firebase";
import { getDocs, query, where } from "firebase/firestore";
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null)
const [loading, setLoading] = useState(true)
function login(email, password) {
return auth.signInWithEmailAndPassword(email, password).then(() => {
const Doc = query(database.usersRef, where("email", "==", email));
getDocs(Doc).then((querySnapshot) => {
let values = '';
querySnapshot.forEach((doc) => {
values = doc.id;
});
var userUpdate = database.usersRef.doc(values);
userUpdate.update({
lastActive: new Date().toLocaleString('en-SG'),
})
})
});
}
function logout() {
return auth.signOut();
}
function forgetPassword(email) {
return auth.sendPasswordResetEmail(email);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
function updateDisplayName(name) {
return currentUser.updateDisplayName(name)
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged( user => {
setLoading(false)
setCurrentUser(user)
})
return unsubscribe
}, [])
const value = {
currentUser,
login,
forgetPassword,
logout,
updateEmail,
updatePassword,
updateDisplayName,
}
return (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
)
}
The initial currentUser state matches the unauthenticated state, so when the app initially renders, if you are accessing a protected route the redirection will occur because the currentUser state hasn't updated yet.
Since onAuthStateChanged returns null for unauthenticated users then I suggest using anything other than null for the initial currentUser state. undefined is a good indeterminant value. You can use this indeterminant value to conditionally render a loading indicator, or nothing at all, while the auth status is confirmed on the initial render.
AuthProvider
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(); // <-- undefined
...
PrivateRoute
const PrivateRoute = (props) => {
const { currentUser } = useAuth();
if (currentUser === undefined) {
return null; // or loading spinner, etc...
}
return currentUser
? (
<Route {...props} />
)
: (
<Redirect
to={{
pathname: "/",
state: {
from: props.location
}
}}
/>
);
}
You should also really replace the window.location = '/portfolio/' + docRef.id; logic with a history.push('/portfolio/' + docRef.id); so you are not unnecessarily reloading the page.
const history = useHistory();
...
async function handleSubmit(e) {
e.preventDefault();
setError('');
setLoading(true);
try {
const docRef = await database.portfolioRef.add({
intervieweeName: intervieweeNameRef.current.value,
intervieweeEmail: intervieweeEmailRef.current.value,
intervieweeMobileNumber: intervieweeMobileRef.current.value,
projectTitle: projectTitleRef.current.value,
portfolioTitle: portfolioNameRef.current.value,
dateCreated: new Date().toLocaleString('en-SG'),
createdBy: currentUser.displayName
});
history.push('/portfolio/' + docRef.id);
} catch (error) {
// handle error, clear loading state
setLoading(false);
}
}
I'm new to the webdev world and want to learn ReactJS. I followed a tutorial I found on YouTube made by Traversy where he makes a task tracker and now I want to make some changes to it to learn and practice some more.
I want to use context for the appointments (originally named tasks in the tutorial), add a calendar with react-calendar and use react-router-dom.
I got stuck for a while trying to make the list render, because it only rendered "empty". Later on found this post with a similar issue to mine: Only run a useEffect fetch after first useEffect fetch has fired and setUser in context
I changed bits of my code based on that post and now it does render the appointment list, but I don't know why it didn't work before and I'm unsure on why it does work now. I don't even know if I'm using context correctly or just prop-drilling. Help would be greatly appreciated. Thank you.
Also, sorry if my code is a mess, I'm new at this.
App.js
import { createContext, useState, useEffect } from "react";
import Dashboard from "./views/Dashboard";
import './App.css';
import { BrowserRouter as Router, Route, Routes} from "react-router-dom";
import AddAppointmentForm from "./views/AddAppointmentForm";
export const AppContext = createContext();
export const AppUpdateContext = createContext();
function App() {
const [appointments, setAppointments] = useState([])
const updateAppointments = (apptList) => {
setAppointments(apptList)
}
return (
<AppContext.Provider value={ appointments }>
<AppUpdateContext.Provider value={ updateAppointments }>
<Router>
<Routes>
<Route path="/" element={<Dashboard appointments={appointments} />} />
{/* <Route path="/add" element={<AddAppointmentForm />} /> TBA */}
</Routes>
</Router>
</AppUpdateContext.Provider>
</AppContext.Provider>
);
}
export default App;
Dashboard.js
import { useEffect, useContext} from "react";
import { AppContext } from "../App";
import { AppUpdateContext } from "../App";
import AppointmentList from "../components/AppointmentList";
import Header from "../components/Header";
// function Dashboard() { // this is how it used to be
function Dashboard(props) {
const appointments = useContext(AppContext)
const setAppointments = useContext(AppUpdateContext)
const fetchAppointmentList = async () => {
const res = await fetch("http://localhost:5000/appointments");
const data = await res.json();
return data;
}
useEffect(() => {
const getAppointments = async () => {
const appointmentsFromServer = await fetchAppointmentList();
setAppointments(appointmentsFromServer);
}
getAppointments();
console.log("ñññññ",appointments)
}, []);
console.log("aagh",appointments)
return (
<div style={dashboardStyle}>
<Header />
{/* {appointments.lenght>0 ? (<AppointmentList />) : <p>empty</p>} this is how it used to be */}
<AppointmentList appointments={props?.appointments}/>
</div>
);
}
const dashboardStyle = {
maxWidth: "31.25rem",
overflow: "auto",
minHeight: "18.75rem",
border: "1px solid steelblue",
margin: "1.875rem auto",
padding: ".5rem",
boxSizing: "border-box",
}
export default Dashboard;
AppointmentList.js
import Appointment from "./Appointment";
import { AppContext } from "../App";
import { useContext } from "react";
function AppointmentList({ appointments }) {
// function AppointmentList() { // this is how it used to be
// const { appointments, setAppointments } = useContext(AppContext)
console.log("appList",appointments) // this is how it used to be
return (
<>
{
appointments.map(appt => (
<Appointment key={appt.id} appointment={appt} />
))
}
</>
);
}
export default AppointmentList;
Why does optional chaining allows rendering when fetching data through
useEffect in an app that uses context?
<AppointmentList appointments={props?.appointments}/>
It allows rendering by preventing accidental accesses into potentially null or undefined objects. The only way props could be undefined though is if you just simply don't declare it, i.e. const Dashboard = () => {.... vs const Dashboard = (props) => {.....
You are drilling the appointments state through props. AppointmentList can use the AppContext context to access the appointments state, while Dashboard can use the AppUpdateContext context to update the appointments state.
App
function App() {
const [appointments, setAppointments] = useState([]);
const updateAppointments = (apptList) => {
setAppointments(apptList);
};
return (
<AppContext.Provider value={{ appointments }}> // <-- need object here
<AppUpdateContext.Provider value={{ updateAppointments }}> // <-- and here
<Router>
<Routes>
<Route path="/" element={<Dashboard />} /> // <-- don't pass props
</Routes>
</Router>
</AppUpdateContext.Provider>
</AppContext.Provider>
);
}
Dashboard
function Dashboard() { // <-- no props
const { updateAppointments } = useContext(AppUpdateContext); // <-- access from context
const fetchAppointmentList = async () => {
const res = await fetch("http://localhost:5000/appointments");
const data = await res.json();
return data;
};
useEffect(() => {
const getAppointments = async () => {
const appointmentsFromServer = await fetchAppointmentList();
updateAppointments(appointmentsFromServer);
}
getAppointments();
}, []);
return (
<div style={dashboardStyle}>
<Header />
<AppointmentList /> // <-- don't pass props
</div>
);
}
AppointmentList
function AppointmentList() { // <-- no props
const { appointments } = useContext(AppContext); // <-- access from context
return appointments.map(appt => (
<Appointment key={appt.id} appointment={appt} />
));
}
I encountered such an error match undefind. I'm taking an old course on React did everything as shown in the lesson but I get an error why. i don't understand why match undefinde. Maybe you need to pick up the match in another way or somehow pass it ??
import React from "react";
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { NavBar } from "./components/NavBar";
import { Home } from './pages/Home'
import { About } from './pages/About'
import { Profile } from './pages/Profile'
import { Alert } from "./components/Alert";
import { AlertState } from "./context/alert/AlertState";
import { GithubState } from "./context/github/GithunState";
function App() {
return (
<GithubState>
<AlertState>
<BrowserRouter>
<NavBar />
<div className="container pt-4">
<Alert alert={{text: 'Test Alert'}} />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/profile/:name" element={<Profile />} />
</Routes>
</div>
</BrowserRouter>
</AlertState>
</GithubState>
);
}
export default App;
import React from 'react'
import { useContext, useEffect } from 'react';
import { GithubContext } from '../context/github/githubContex';
export const Profile = ({match}) => {
// const github = useContext(GithubContext)
// const name = match.params.name
// useEffect(() => {
// github.getUser()
// github.getRepos(name)
// }, [])
console.log('asd',match);
return(
<div>
<h1>Profile page</h1>
</div>
)
}
import React, {useReducer} from "react"
import axios from 'axios'
import { CLEAR_USERS, GET_REPOS, GET_USER, SEARCH_USERS, SET_LOADING } from "../types"
import { GithubContext } from "./githubContex"
import { githubReducer } from "./githubReducer"
const CLIENT_ID = process.env.REACT_APP_CLIENT_ID
const CLIENT_SECRET = process.env.REACT_APP_CLIENT_SECRET
const withCreads = url => {
return `${url}client_id=${CLIENT_ID}&client_secret=${CLIENT_SECRET}`
}
export const GithubState = ({children}) => {
const initialState = {
user: {},
users: [],
loading: false,
repos: []
}
const [state, dispatch] = useReducer(githubReducer, initialState)
const search = async value => {
setLoading()
const response = await axios.get(
withCreads(`https://api.github.com/search/users?q=${value}&`)
)
dispatch({
type: SEARCH_USERS,
payload: response.data.items
})
}
const getUser = async name => {
setLoading()
const response = await axios.get(
withCreads(`https://api.github.com/users/users/${name}?`)
)
dispatch({
type: GET_USER,
payload: response.data
})
}
const getRepos = async name => {
setLoading()
const response = await axios.get(
withCreads(`https://api.github.com/users/users/${name}/repos?per_page=5&`)
)
dispatch({
type: GET_REPOS,
payload: response.data
})
}
const clearUsers = () => dispatch({type: CLEAR_USERS})
const setLoading = () => dispatch({type: SET_LOADING})
const {user, users, repos, loading} = state
return (
<GithubContext.Provider value={{
setLoading, search, getUser, getRepos, clearUsers,
user, users, repos, loading
}}>
{children}
</GithubContext.Provider>
)
}
link to Github https://github.com/Eater228/React-Hooks
Check your package.json file and if you are using an older version of react-router-dom please use the latest version.
match prop should be passed down from the Route component and it will reflect the correct data as you are using react-router-dom.
Update
You are using element prop for rendering component and that's not the correct one. You should replace that element with component and it will work.
Update
Please consider using useParams hook instead of that match prop.
https://reactrouter.com/docs/en/v6/getting-started/overview#reading-url-parameters
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;
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' },
);