How to check if Session is alive with firebase + react? - javascript

I've added an authorization with firebase, which works completely fine.
It is possible to login to the app, and navigate, but when I use firebase.auth().signOut the onAuthChanged observable is not changed/not triggered.
For correct login/password(400 for the wrong combination) - the session is saved, and I have the user credentials:
import React, {useContext, useEffect} from 'react';
import {ROUTES} from '../../../constants';
import {AuthUserContext} from "../../../session";
import history from '../../../helpers/history';
import {useLocation} from "react-router";
import app from "../../../api/firebase";
const WithAuthorization: React.FC = ({children}) => {
const authUser = useContext(AuthUserContext);
const isLogin = useLocation().pathname === ROUTES.LOGIN;
const pushLogin = () => !isLogin && history.push(ROUTES.LOGIN);
useEffect(() => {
const listener = app.auth().onAuthStateChanged(
(user: any) => {
if(!user) {
pushLogin()
} else {
console.log('Signed in with user');
console.log(user);
}
},
(e: any) => {
console.log(e);
}, () => {
console.log('completed');
});
return listener();
}, [])
return <>
{authUser ? children : pushLogin()}
</>;
}
export default WithAuthorization;
But then, when the application is refreshed, I want to check if the session is alive.
While looking through the docs I've found onAuthChanged observable, which seems pretty straight-forward, but it is actually triggered only when I log in.
After the page is refreshed, or when I trigger signOut - it does nothing.
This is the authorization protection component, that wraps the entire App:
import React, {useContext, useEffect} from 'react';
import {ROUTES} from '../../../constants';
import {AuthUserContext} from "../../../session";
import history from '../../../helpers/history';
import app from "../../../api/firebase";
const WithAuthorization: React.FC = ({children}) => {
const authUser = useContext(AuthUserContext);
const pushLogin = () => history.push(ROUTES.LOGIN);
useEffect(() => {
const listener = app.auth().onAuthStateChanged(
(user: any) => {
if(!user) pushLogin()
},
(e: any) => {
console.log(e);
}, () => {
console.log('completed');
});
return listener();
}, [])
return <>
{authUser ? children : pushLogin()}
</>;
}
export default WithAuthorization;
Am I missing something with the auth protection component or observable?
--- The app structure:
The App component is quite simple:
import React, {useState} from 'react';
import { Route, Switch, useLocation } from 'react-router';
import { Header, WithAuthorization } from './common';
import DeviceSelection from './DeviceSelection';
import PerfectScroll from 'react-perfect-scrollbar';
import NotFound from './NotFound';
import ThankYou from "./Thankyou";
import 'react-perfect-scrollbar/dist/css/styles.css';
import './App.scss';
import {ROUTES} from "../constants";
import Login from "./Login";
import {AuthUserContext} from "../session";
const App = () => {
const {pathname} = useLocation();
const [authUser, setAuthUser] = useState(null as any);
const isThankYou = pathname === ROUTES.THANKYOU;
return (
<AuthUserContext.Provider
value={authUser}
>
<WithAuthorization>
{!isThankYou && <Header authUser={authUser}/>}
</WithAuthorization>
<div className={`${!isThankYou ? 'appScrollContainer' : ''}`}>
<PerfectScroll>
<Switch>
<Route exact path={[ROUTES.ROOT, ROUTES.HOME]} component={() => <WithAuthorization><DeviceSelection/></WithAuthorization>} />
<Route path={ROUTES.THANKYOU} component={() => <WithAuthorization><ThankYou/></WithAuthorization>} />
<Route path={ROUTES.LOGIN} component={() => <Login setAuthUser={(user: any) => setAuthUser(user)} />}/>
<Route path="*" component={NotFound} />
</Switch>
</PerfectScroll>
</div>
</AuthUserContext.Provider>
);
}
export default App;
Signout is coming from a button, inside Header, which is also wrapped in WithAuthorization:
<Button label={'Sign out'} click={() => app.auth().signOut()} />
Login does only one 1 thing, redirects to /home if login was successful:
import React, {useState} from 'react';
import TextInput from "../common/TextInput";
import history from '../../helpers/history';
import {ROUTES} from "../../constants";
import app, {signInWithEmailAndPassword} from "../../api/firebase";
interface Props {
setAuthUser: (user: any) => void,
}
const Login: React.FC<Props> = ({setAuthUser}) => {
const [form, updateForm] = useState({login: '', password: ''});
const authorize = (user: string, password: string) => {
app.auth().setPersistence(app.auth.Auth.Persistence.SESSION)
.then(() => {
return signInWithEmailAndPassword(user, password).then((user: any) => {
if(user) {
setAuthUser(user);
history.push(ROUTES.ROOT);
return user
}
return null
})
})
.catch((e: any) => {
console.log(e);
})
}
return <div className='form'>
<TextInput
type="text"
placeholder='login'
name={'login'}
value={form.login}
label='Login'
onChange={(e) => updateForm({...form, login: e.currentTarget.value})}
/>
<TextInput
type="password"
placeholder='password'
name={'password'}
value={form.password}
label='Password'
onChange={(e) => updateForm({...form, password: e.currentTarget.value})}
/>
<button onClick={() => authorize(form.login, form.password)}>Submit</button>
</div>
}
export default Login;
FIrebase usage itself:
import firebase from 'firebase/app';
import 'firebase/firestore';
import 'firebase/auth';
import {DEV_LOCAL_CONFIG, DEV_REMOTE_CONFIG, ORDERS_COLLECTION} from "./const";
firebase.initializeApp(window.location.hostname !== 'localhost' ? DEV_LOCAL_CONFIG : DEV_REMOTE_CONFIG);
/* ==== Authorization ==== */
const signInWithEmailAndPassword = (email: string, password: string) =>
firebase.auth().signInWithEmailAndPassword(email, password);
const signOut = () => firebase.auth().signOut();
export default firebase;
export {
signInWithEmailAndPassword,
signOut
}

My mistake was with this line only:
return listener();
When I define listener in useEffect, it is unsubscribed immediately.
Should be:
return () => listener()
Other than this, everything works fine.

Related

useContext() returns null value

I am trying to use useContext to get the current logged in user details. I am getting an error in my console which says:
Uncaught TypeError: user is null
children Animate.jsx:79
Animate Animate.jsx:76
I am getting this error whenever I Logout. I am facing no errors in the console when I am logged in.
Here is the code:
Context.js File:
import { onAuthStateChanged } from "firebase/auth";
import { useEffect, useState } from "react";
import { createContext } from "react";
import { auth } from "./firebase";
export const Context = createContext()
const ContextProviderfunc=({children})=>{
const use={
"mail":'history'
}
const [user,setuser]=useState({});
useEffect(()=>{
const unsubscribe = onAuthStateChanged(auth,(currentUser)=>{
// console.log(currentUser);
setuser(currentUser);
})
return ()=>{
unsubscribe();
}
},[])
return(
<Context.Provider value={user}>
{children}
</Context.Provider>
)
}
export default ContextProviderfunc
Animate.jsx File:
import React from 'react'
import {
BrowserRouter as Router, Routes, Route, useLocation, Link
} from 'react-router-dom'
import Navbar from './Navbar'
import RecipeCard from './RecipeCard'
import Details from './Details'
import { AnimatePresence, motion } from "framer-motion"
import Add from './Add'
import clicked from './RecipeCard'
import { useState } from 'react'
import Login from './Login'
import Register from './Register'
import Protected from './Protected'
import Logout from './Logout'
import Myrecipe from './Myrecipe'
import { useContext } from 'react'
import { Context } from '../Context'
export default function Animate(props) {
const user=useContext(Context)
const [pname, setpname] = useState()
function assign(nigga) {
setpname(nigga);
console.log(nigga);
}
const location = useLocation()
return (
<motion.div className='all'
>
<AnimatePresence>
<Routes location={location} key={location.pathname}>
<Route path='/Login' element={<Login />} />
<Route path='/Register' element={<Register />} />
<Route path='*' element={[<Protected><Navbar /></Protected>, <h2>Recipes</h2>,
<div className='recipes'>
{props.query ? props.query.map((object, i) => (
<Link className='lin' to='/Details'>
<div onClick={() => assign(object.Name)}>
<Protected><RecipeCard
src={object.src}
name={object.Name}
ingredients={object.Ingredients}
steps={object.Steps}
key={i}
/></Protected>
</div></Link>
)) : "Loading"}
<Link className='lin' to='/Add'>
<div className='cardcont'>
<img src='https://cdn.pixabay.com/photo/2017/11/10/05/24/add-2935429_960_720.png' alt="1" className='cardimg' />
<div className='cardbody'>
<h3>Add</h3>
</div>
</div></Link>
</div>
]} />
<Route path='/MyRecipe' element={[<Protected><Navbar /></Protected>, <h2>My Recipes</h2>,
<Protected>
<div className='recipes'>
{props.query2 ? props.query2.map((object, i) => {
if(user.email==object.email){
console.log(user.email);
return(<Link className='lin' to='/Details'>
<div onClick={() => assign(object.Name)}>
<Protected><RecipeCard
src={object.src}
name={object.Name}
ingredients={object.Ingredients}
steps={object.Steps}
key={i}
/></Protected>
</div></Link>)
}
}
) : "Loading"}
<Link className='lin' to='/Add'>
<div className='cardcont'>
<img src='https://cdn.pixabay.com/photo/2017/11/10/05/24/add-2935429_960_720.png' alt="1" className='cardimg' />
<div className='cardbody'>
<h3>Add</h3>
</div>
</div></Link>
</div>
</Protected>
]} />
<Route path='/Details' element={
[<Navbar />, props.query ? props.query.map((object, m) => {
if (pname == object.Name) {
console.log(object.Name);
return (<Details
src={object.src}
name={object.Name}
Ingredients={object.Ingredients}
Steps={object.Steps}
key={m}
/>)
}
}) : "Loading"]} />
<Route path='/Add' element={[<Navbar />, <Add />]} />
</Routes>
</AnimatePresence>
</motion.div>
)
}
line 79 of Animate.jsx
if(user.email==object.email){
console.log(user.email);
App.js file:
import Navbar from './components/Navbar';
import RecipeCard from './components/RecipeCard';
import { useContext, useEffect, useState } from 'react';
import { auth, db, storage } from './firebase'
import { collection, addDoc, getDocs, doc } from "firebase/firestore";
import { async } from '#firebase/util';
import Details from './components/Details'
import Animate from './components/Animate';
import {BrowserRouter as Router, Route,Routes} from 'react-router-dom'
import Add from './components/Add';
import ContextProviderfunc from './Context';
import { Context } from './Context';
function App() {
const [query, setquery] = useState()
const [query2, setquery2] = useState()
const [recipe, setrecipe] = useState()
const add = async (e) => {
e.preventDefault()
try {
const docRef= await addDoc(collection(db, "recipe"),
{
src: 'https://images.immediate.co.uk/production/volatile/sites/30/2017/02/Two-panna-cotta-on-plates-298e616.jpg',
ingredients: ['Dish'],
steps: ['Description']
}, {
src: 'https://imagesvc.meredithcorp.io/v3/mm/image?url=https%3A%2F%2Fstatic.onecms.io%2Fwp-content%2Fuploads%2Fsites%2F43%2F2022%2F04%2F19%2F22749-the-best-banana-pudding-mfs-366-1x1-1.jpg',
ingredients: ['Dish'],
steps: ['Description']
});
// console.log('morein');
// console.log("Document written with ID: ", docRef.id);
} catch (error) {
console.log('morein');
console.error("Error adding document: ", error);
}
}
useEffect(() => {
const getrecipe = async () => {
const data = await getDocs(collection(db, 'recipe'))
const data2 = await getDocs(collection(db, 'userrecipe'))
console.log(data);
setquery(data.docs.map((doc) => (
{
...doc.data(), id: doc.id
})
))
setquery2(data2.docs.map((doc) => (
{
...doc.data(), id: doc.id
})
))
console.log(query);
};
getrecipe();
}, []);
useEffect(() => {
// searchRecipes();
}, [])
return (
<div>
<ContextProviderfunc>
<Router>
<Animate query={query} query2={query2}/>
</Router>
</ContextProviderfunc>
</div>
);
}
export default App;
These two files I think are needed to debug. If any more code is needed I will provide it. Please find out the issue in the above code
Whenever the user logs out, value of currentUser will become null in here:
onAuthStateChanged(auth,(currentUser)=>{
// console.log(currentUser);
setuser(currentUser); // currentUser is null
})
Since you are updating the state (setuser), The Context Provider rerenders and passes down null as value.
return(
<Context.Provider value={user}> // user is null, and is being passed down
{children}
</Context.Provider>
)
Within Animate you receive the null value.
const user=useContext(Context) // user is null, after loging out.
Long story short, user is at times null within Animate, so you don't have the guarantee to use properties like email, like this line below:
if(user.email==object.email){
console.log(user.email);
The solution: Wherever you need to use user, first check if it equals to null.
If you set logged out user ti be null, you should not expect to retrieve email address of null.
You should check (read from context) if user still logged in.
For example :
if(user) {
return <div>I am logged in user and my email is {user.email} </div>
}
return <div>You are not logged In </div>
Specific to your case: check first user and then user.email to avoid Uncaught TypeError: user is null
if(user && user.email === object.email) .....
This is just a friendly suggestion, which is not related with your question: Use ===instead of == for for information you can read this page

I try to lay out a match for receiving a name from url but I receive a match undefinde

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

react-js testing library unable to test useState hook and asynchronous axios signIn function

I cant seem to get the test for my reactjs login component to work.
I am using react-testing-library only and not Enzyme.
I want to be able test 3 things:
That the signIn is clicked once and the axios ajax call uses the mocked ajax
That I can set a value using useState and check the value was set
That the page rendered after a successful login has the text welcome
None of the above is working. I have been working through different issues eg this expect(signIn).toHaveBeenCalledTimes(1) returns:
Expected number of calls: 1
Received number of calls: 0
Other times it throws Error: Unable to find the "window" object for the given node. for this:
const button = screen.getByRole('button', {name: /submit/i})
Here is what Login.test.js looks like
/**
* #jest-environment jsdom
*/
import React from 'react';
import { screen, render, waitFor, fireEvent, getAllByLabelText } from '#testing-library/react';
import {renderHook} from '#testing-library/react-hooks'
import userEvent from '#testing-library/user-event';
import { act } from "react-dom/test-utils";
import Login from "../components/auth/login"
import Index from "../components/index"
import {userContext} from "../utils/context/UserContext"
import axios from 'axios';
const user = {user: {} }
const isLoggedIn = false
const setIsLoggedIn = jest.fn()
const setUser = jest.fn()
const setState = jest.fn();
jest.mock('axios');
describe("Login", () => {
afterEach(() => {
jest.clearAllMocks();
});
test('ajax login', async () => {
const {container} = render(
<userContext.Provider value={ {user, isLoggedIn, setIsLoggedIn, setUser} } >
<Login />
</userContext.Provider>
)
axios.post.mockImplementationOnce(() => Promise.resolve({
status: 200,
data: {user: {name: 'Rony', email: 'rony#example.com' } }
}))
setIsLoggedIn.mockReturnValue(true)
const signIn = jest.spyOn(React, "useState")
signIn.mockImplementation(isLoggedIn => [isLoggedIn, setIsLoggedIn]);
const email = container.querySelector('input[name=email]')
const password = container.querySelector('input[name=password]')
const button = screen.getByRole('button', {name: /submit/i})
await act(async () => {
userEvent.type('email', 'abc#yahoo.com')
userEvent.type('password', 'abcddef')
userEvent.click(button)
});
expect(signIn).toHaveBeenCalledTimes(1)
expect(setIsLoggedIn).toEqual(true);
expect(screen.getByText(/welcome/i)).resolves.toBeInTheDocument
})
})
The login component is shown below:
import React, {useState, useEffect, useContext} from 'react';
import PropTypes from 'prop-types';
import Cookies from 'js-cookie';
import axios from 'axios'
import {userContext} from "../../utils/context/UserContext"
export const Login = (props) => {
const {user, setIsLoggedIn, isLoggedIn, setUser} = useContext(userContext)
const token = Cookies.get('csrf_token') || props.csrf_token || ''
const[loginData, setLoginData] = useState({email: '', password: '' })
const[csrf_token, setCsrfToken] = useState('')
useEffect(() => {
setCsrfToken(token)
}, [])
const handleChange = (event) => {
const {name, value} = event.target
setLoginData({...loginData, [name]: value})
}
const signIn = (event) => {
event.preventDefault();
let headers = {
headers: {'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrf_token
}
}
let url = "/api/v1/sessions"
axios.post(url, {user: loginData}, headers)
.then(response => {
setLoginData({email: '', password: ''})
setIsLoggedIn(true)
setUser(response.data.user)
})
.catch(function (error) {
setIsLoggedIn(false)
})
}
return(
<React.Fragment>
<div> </div>
<form onSubmit={signIn}>
<h3>Login</h3>
<div className="mb-3 form-group col-xs-4">
<label htmlFor="exampleInputEmail1" className="form-label">Email address</label>
<input type="email" name="email" className="form-control" id="exampleInputEmail1"
value={loginData.email} onChange={handleChange} aria-describedby="emailHelp" />
<div id="emailHelp" className="form-text">We'll never share your email with anyone else.</div>
</div>
<div className="mb-3">
<label htmlFor="exampleInputPassword1" className="form-label">Password</label>
<input type="password" name="password" className="form-control" id="exampleInputPassword1"
value={loginData.password} onChange={handleChange} />
</div>
<div className="mb-3 form-check">
<input type="checkbox" className="form-check-input" id="exampleCheck1" />
<label className="form-check-label" htmlFor="exampleCheck1">Check me out</label>
</div>
<button name="submit" type="submit" className="btn btn-primary" onClick={signIn} >Submit</button>
</form>
</React.Fragment>
)
}
UserContext.js
import React from 'react';
export const userContext = React.createContext(undefined)
The index.js
import React, {useState, useEffect, useContext} from 'react';
import PropTypes from 'prop-types';
import Cookies from 'js-cookie';
import axios from 'axios'
import {userContext} from "../../utils/context/UserContext"
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import Login from "../components/auth/login";
export default const Index = (props) => {
let form_token = Cookies.get('csrf_token') || ''
let check_login = props.isLoggedIn || false
let check_user = props.user || ""
const[isLoggedIn, setIsLoggedIn] = useState(check_login)
const[user, setUser] = useState(check_user)
const[isError, setIsError] = useState('')
useEffect(() => {
setCsrfToken(form_token)
}, [isLoggedIn, csrf_token, user])
return (
<userContext.Provider value={{user, setUser, isLoggedIn, setIsLoggedIn}}>
<Router>
<React.Fragment>
<div>
<Switch>
<Route exact path='/'>
</Route>
<Route path='/login'>
<Login />
</Route>
</Switch>
</div>
</React.Fragment>
</Router>
</userContext.Provider>
)
}
You should not mock React's useState function, and you cannot test redirection because you are rendering only the Login component in your test.
Beside this I don't thinks you can mock a function defined in a functional component (signIn), and even if it was possible you should not test internal implementation, but only what a real user whould see : here, you can test that the inputs have the correct value after the userEvent.type(...).
Just remove the useState and signIn mocks, then in your Login component add a redirection after a successful login. Then you just have to enter user information, click the submit button (as you already do), then you can check that history.push have been called correctly (see Simplest test for react-router's Link with #testing-library/react).

Authentication with Next.js and Redux

Here is my situation. I am trying to auth the user on the load and need to run the authentication in the pages/_app.js file. Here is the error useReduxContext.js:24 Uncaught Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider> . Is there a way to run the auth in a wrapped <Provider>. Here is the link to codesandbox
file pages/_app.js
import React, { useEffect } from 'react';
import { Provider, useDispatch } from 'react-redux';
import { auth } from '../lib/firebase.js';
import { getUserLoggedIn } from '../store/user.js';
import configureAppStore from '../store/configureAppStore.js';
import Header from '../components/nav/Header.jsx';
const store = configureAppStore();
function MyApp({ Component, pageProps }) {
const dispatch = useDispatch();
// to check firebase auth state
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (user) {
const idTokenResult = await user.getIdTokenResult();
dispatch(
getUserLoggedIn({
email: user.email,
token: idTokenResult.token,
})
);
}
});
// cleanup
return () => unsubscribe();
}, []);
return (
<>
<Provider store={store}>
<Header />
<Component {...pageProps} />
</Provider>
</>
);
}
export default MyApp;
A simple solution to this problem could be creating an empty component that runs the auth for you in a useEffect.
import React, { useEffect } from 'react';
import { Provider, useDispatch } from 'react-redux';
import { auth } from '../lib/firebase.js';
import { getUserLoggedIn } from '../store/user.js';
import configureAppStore from '../store/configureAppStore.js';
import Header from '../components/nav/Header.jsx';
const store = configureAppStore();
function MyApp({ Component, pageProps }) {
const AuthComponent = React.memo(() => {
const dispatch = useDispatch();
// to check firebase auth state
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(async (user) => {
if (user) {
const idTokenResult = await user.getIdTokenResult();
dispatch(
getUserLoggedIn({
email: user.email,
token: idTokenResult.token,
})
);
}
});
// cleanup
return () => unsubscribe();
}, []);
return null;
})
return (
<>
<Provider store={store}>
<AuthComponent />
<Header />
<Component {...pageProps}
/>
</Provider>
</>
);
}
export default MyApp;

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

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' },
);

Categories

Resources