useEffect cleanup function? Memory leak error - reactjs

I want to understand why don't work properly the useEffect hook without the AbortionController.abort function.
I have a nested route in the app.js like:
<Route path='/profile' element={<PrivateRoute />}>
<Route path='/profile' element={<Profile />} />
</Route>
than the two component:
PrivateRoute:
import { Navigate, Outlet } from 'react-router-dom';
import { useAuthStatus } from '../hooks/useAuthStatus';
export default function PrivateRoute() {
const { loggedIn, checkingStatus } = useAuthStatus();
if (checkingStatus) return <h1>Loading...</h1>;
return loggedIn ? <Outlet /> : <Navigate to='/sign-in' />;
}
Profile:
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { getAuth, updateProfile } from 'firebase/auth';
import { updateDoc, doc } from 'firebase/firestore';
import { db } from '../firebase.config';
import { toast } from 'react-toastify';
export default function Profile() {
const auth = getAuth();
const [changeDetails, setChangeDetails] = useState(false);
const [formData, setFormData] = useState({
name: auth.currentUser.displayName,
email: auth.currentUser.email,
});
const { name, email } = formData;
const navigate = useNavigate();
const onLogout = () => {
auth.signOut();
navigate('/');
};
const onSubmit = async (e) => {
try {
if (auth.currentUser.displayName !== name) {
//update display name if fb
await updateProfile(auth.currentUser, {
displayName: name,
});
//update in firestore
const userRef = doc(db, 'users', auth.currentUser.uid);
await updateDoc(userRef, {
name,
});
}
} catch (error) {
toast.error('Could not update profile details');
}
};
const onChange = (e) => {
setFormData((prev) => ({
...prev,
[e.target.id]: e.target.value,
}));
};
return (
<div className='profile'>
<header className='profileHeader'>
<p className='pageHeader'>My Profile</p>
<button type='button' className='logOut' onClick={onLogout}>
Logout
</button>
</header>
<main>
<div className='profileDetailsHeader'>
<p className='profileDetailsText'>Personal Details</p>
<p
className='changePersonalDetails'
onClick={() => {
setChangeDetails((prev) => !prev);
changeDetails && onSubmit();
}}
>
{changeDetails ? 'done' : 'change'}
</p>
</div>
<div className='profileCard'>
<form>
<input
type='text'
id='name'
className={!changeDetails ? 'profileName' : 'profileNameActive'}
disabled={!changeDetails}
value={name}
onChange={onChange}
/>
<input
type='text'
id='email'
className={!changeDetails ? 'profileEmail' : 'profileEmailActive'}
disabled={!changeDetails}
value={email}
onChange={onChange}
/>
</form>
</div>
</main>
</div>
);
}
and the custom Hook:
import { useEffect, useState } from 'react';
import { getAuth, onAuthStateChanged } from 'firebase/auth';
export function useAuthStatus() {
const [loggedIn, setLoggedIn] = useState(false);
const [checkingStatus, setCheckingStatus] = useState(true);
useEffect(() => {
const abortCont = new AbortController();
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
setCheckingStatus(false);
});
// return () => abortCont.abort();
}, [setLoggedIn, setCheckingStatus]);
return { loggedIn, checkingStatus };
}
Can you explain me why do I get the error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
I found the solution with the AbortController, but still don't understand what is the problem.
The error appears randomly, sometimes when I log in, sometimes when I'm not logged in, and try to go on the profile page. The app works fine, just want to understand what happens under the hood.
If I understand, if I'm not logged in, then it will rendered the 'Sign-in' page, if I'm logged in, then the 'Profile' page will be rendered, also there is a loading page but it's not the case. So, it's simple, if I'm logged in render this page, if not, render the other page. So where is the Problem? Why do I need the AbortController function?

onAuthStateChanges will listen forever in your useEffect hook. You need to unsubscribe every time the hook is run otherwise you will have these memory leaks. In your case the change of the users auth state will try to called setLoggedIn even when the component has been unmounted.
Looking at the documentation for onAuthStateChanged (https://firebase.google.com/docs/reference/js/v8/firebase.auth.Auth#onauthstatechanged) it returns a firebase.Unsubscribe.
You'll have to do something like this:
useEffect(() => {
const auth = getAuth();
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setLoggedIn(true);
} else {
setLoggedIn(false);
}
setCheckingStatus(false);
});
return () => unsubscribe();
})
The callback you can optionally return in a useEffect hook is used for cleanup on subsequent calls.

Related

Function setDoc() called with invalid data. Unsupported field value: a custom UserImpl object (found in field owner in document CreatedClasses)

This is the first time I'm asking a question here and also a newbie to coding. I'm trying to clone google classroom.
I am trying to use firestore to make a db collection when creating the class. But when I click create it doesn't create the class and create the db in firestore. It shows that the setDoc() function is invalid. Im using firestore version 9 (modular)
Here is my Form.js file. (The firestore related code is also included here)
import { DialogActions, TextField , Button} from "#material-ui/core"
import React, {useState} from 'react'
import { useLocalContext, useAuth } from '../../../context/AuthContext'
import { v4 as uuidV4 } from 'uuid'
import { db} from '../../../firebase'
import { collection, doc, setDoc } from "firebase/firestore"
const Form = () => {
const [className, setClassName] = useState('')
const [Level, setLevel] = useState('')
const [Batch, setBatch] = useState('')
const [Institute, setInstitute] = useState('')
const {setCreateClassDialog} = useLocalContext();
const {currentUser} = useAuth();
const addClass = async (e) => {
e.preventDefault()
const id = uuidV4()
// Add a new document with a generated id
const createClasses = doc(collection(db, 'CreatedClasses'));
await setDoc(createClasses, {
owner:currentUser,
className: className,
level: Level,
batch: Batch,
institute: Institute,
id: id
}).then (() => {
setCreateClassDialog(false);
})
}
return (
<div className='form'>
<p className="class__title">Create Class</p>
<div className='form__inputs'>
<TextField
id="filled-basic"
label="Class Name (Required)"
className="form__input"
variant="filled"
value={className}
onChange={(e) => setClassName(e.target.value)}
/>
<TextField
id="filled-basic"
label="Level/Semester (Required)"
className="form__input"
variant="filled"
value={Level}
onChange={(e) => setLevel(e.target.value)}
/>
<TextField
id="filled-basic"
label="Batch (Required)"
className="form__input"
variant="filled"
value={Batch}
onChange={(e) => setBatch(e.target.value)}
/>
<TextField
id="filled-basic"
label="Institute Name"
className="form__input"
variant="filled"
value={Institute}
onChange={(e) => setInstitute(e.target.value)}
/>
</div>
<DialogActions>
<Button onClick={addClass} color='primary'>
Create
</Button>
</DialogActions>
</div>
)
}
export default Form
And also (I don't know whether this is helpful but my context file is below)
import React, { createContext, useContext, useEffect, useState } from "react";
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
onAuthStateChanged,
signOut,
GoogleAuthProvider,
signInWithPopup
} from "firebase/auth";
import { auth } from "../firebase";
const AuthContext = createContext();
const AddContext = createContext()
export function useAuth() {
return useContext(AuthContext);
}
export function useLocalContext(){
return useContext(AddContext)
}
export function ContextProvider({children}){
const [createClassDialog,setCreateClassDialog] = useState(false);
const [joinClassDialog, setJoinClassDialog] = useState(false);
const value = { createClassDialog, setCreateClassDialog, joinClassDialog, setJoinClassDialog };
return <AddContext.Provider value={value}> {children} </AddContext.Provider>;
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true)
function signup(email, password) {
return createUserWithEmailAndPassword(auth,email, password);
}
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
function logout() {
return signOut(auth);
}
function resetPassword(email) {
return auth.sendPasswordResetEmail(email)
}
function googleSignIn() {
const googleAuthProvider = new GoogleAuthProvider();
return signInWithPopup(auth, googleAuthProvider);
}
function updateEmail(email) {
return currentUser.updateEmail(email)
}
function updatePassword(password) {
return currentUser.updatePassword(password)
}
useEffect(() => {
const unsubscribe = onAuthStateChanged( auth, (user) => {
setCurrentUser(user);
setLoading(false)
});
return () => {
unsubscribe();
};
}, []);
return (
<AuthContext.Provider
value={{ currentUser, login, signup, logout, googleSignIn, resetPassword,updateEmail, updatePassword }}
>
{!loading && children}
</AuthContext.Provider>
);
}
The console error message:
Try something like this, excluding the collection function from setting the document.
// Add a new document with a generated id
await setDoc(doc(db, 'CreatedClasses'), {
owner:currentUser,
className: className,
level: Level,
batch: Batch,
institute: Institute,
id: classId
}).then (() => {
setCreateClassDialog(false);
})

Reactjs redirect to dashboard page after successful login with react-router-dom (v6)

I'm working simple reactjs login form with redux/toolkit. I wanted to redirect to dashboard page after successful login. It's throwing following error. I'm new to reactjs and please let me know if I missed anything.
Error:
Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component.
authSlice.js
import { useNavigate } from 'react-router-dom';
export const submitLogin =
({
email,
password
}) =>
async dispatch => {
const history = useNavigate();
return jwtService
.signInWithEmailAndPassword(email, password)
.then(user => {
history('/dashboard');
return dispatch(loginSuccess());
})
.catch(error => {
return dispatch(loginError(error));
});
};
const authSlice = createSlice({
name: 'auth',
initialState,
reducers: {
loginSuccess: ....
loginError: ....
logoutSuccess: ....
},
extraReducers: {},
});
export const { loginSuccess, loginError, logoutSuccess } = authSlice.actions;
export default authSlice.reducer;
Login.js
const Login = () => {
function handleSubmit(model) {
dispatch(submitLogin(model));
}
return (
<Formsy onValidSubmit={handleSubmit} ref={formRef}>
<input type="text" placeholder="username" />
....
</Formsy>
)
}
App.js
<Routes>
<Route path="/dashboard" element={<ProtectedRoutes />}>
<Route path="" element={<Dashboard />} />
</Route>
<Route exact path="/" element={<Login />} />
<Route path="*" element={<PageNotFound />} />
</Routes>
import React from 'react';
import { useSelector } from 'react-redux';
import { Navigate, Outlet } from 'react-router-dom';
const useAuth = () => {
const auth = useSelector(({ auth }) => auth);
return auth && auth.loggedIn;
};
const ProtectedRoutes = () => {
const isAuth = useAuth();
return isAuth ? <Outlet /> : <Navigate to="/" />
}
export default ProtectedRoutes;
Issue
You are attempting to use the useNavigate hook outside a React component, which is an invalid use. React hooks must be called at the top-level, they cannot be called conditionally, in functions, loops, etc...
Rules of hooks
Solutions
Pass the navigate function to the submitLogin action creator.
export const submitLogin = ({ email, password }, navigate) =>
async dispatch => {
return jwtService
.signInWithEmailAndPassword(email, password)
.then(user => {
navigate('/dashboard');
return dispatch(loginSuccess());
})
.catch(error => {
return dispatch(loginError(error));
});
};
...
const Login = () => {
const navigate = useNavigate();
function handleSubmit(model) {
dispatch(submitLogin(model, navigate));
}
return (
<Formsy onValidSubmit={handleSubmit} ref={formRef}>
<input type="text" placeholder="username" />
....
</Formsy>
);
}
Chain/await the returned Promise
export const submitLogin = ({ email, password }) =>
async dispatch => {
return jwtService
.signInWithEmailAndPassword(email, password)
.then(user => {
dispatch(loginSuccess());
return user;
})
.catch(error => {
dispatch(loginError(error));
throw error;
});
};
...
const Login = () => {
const navigate = useNavigate();
async function handleSubmit(model) {
try {
await dispatch(submitLogin(model));
navigate('/dashboard');
} catch(error) {
// handle error, log, etc...
}
}
return (
<Formsy onValidSubmit={handleSubmit} ref={formRef}>
<input type="text" placeholder="username" />
....
</Formsy>
);
}
When you use react hooks , there are some rules you can't bypass them
You have to call useHook() at the root level of your component or
function
You can't make conditional useHook() call
I think but maybe i am wrong that error occurred in this part of your code
export const submitLogin =
({
email,
password
}) =>
async dispatch => {
const history = useNavigate(); // x Wrong you can't call inside another function
...
};

Router.push makes page flash and changes url to localhost:3000/?

After pushing a route in NextJS the path seems to be valid for a split of a second http://localhost:3000/search?query=abc and then changes to http://localhost:3000/?. Not sure why this is happening.
I have tried it with both import Router from 'next/router' and import { useRouter } from 'next/router'. Same problem for both import types.
Here's my component and I use the route.push once user submits a search form.
import React, { useEffect, useState } from "react";
import Router from 'next/router';
const SearchInput = () => {
const [searchValue, setSearchValue] = useState("");
const [isSearching, setIsSearching] = useState(false);
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (isSearching) {
Router.push({
pathname: "/search",
query: { query: searchValue },
});
setIsSearching(false);
}
}, [isSearching, searchValue]);
const handleSearch = () => {
if (searchValue) {
setIsSearching(true);
}
};
return (
<form onSubmit={handleSearch}>
<input
value={searchValue}
onChange={(event) => setSearchValue(event.target.value)}
placeholder="Search"
/>
</form>
);
};
The default behavior of form submissions to refresh the browser and render a new HTML page.
You need to call e.preventDefault() inside handleSearch.
const handleSearch = (e) => {
e.preventDefault()
if (searchValue) {
setIsSearching(true);
}
};

useEffect keep getting executed every time even dependency not changed

We have UserContext which sets user object which we can use throughout application. Our UserContext keep executing every time and unnecessary making api call even though dependency hasn't changed.
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Whenever we navigate to different page in our react app, we are seeing "User" route being called every time even though token isn't updated. Our token changes only when user log off.
Our AppRouter looks like following;
import React from 'react';
import AppRouter from "./AppRouter";
import { Container } from 'react-bootstrap';
import Header from './components/Header';
import { ToastProvider, DefaultToastContainer } from 'react-toast-notifications';
import 'bootstrap/dist/css/bootstrap.css';
import './scss/styles.scss';
import { UserContextProvider } from './UserContextProvider';
export default function App() {
const ToastContainer = (props) => (
<DefaultToastContainer
className="toast-container"
style={{ zIndex:100,top:50 }}
{...props}
/>
);
return (
<UserContextProvider>
<ToastProvider autoDismiss={true} autoDismissTimeout={3000} components={{ ToastContainer }}>
<Container fluid>
<Header />
<AppRouter />
</Container>
</ToastProvider>
</UserContextProvider>
)
}
This is our internal app so we want user to be logged in for 30 days and they don't have to keep login every time. So when user login first time, we create a token for them and keep that token in cookies. So if user close the browser and come back again, we check token in cookies. If token exists, we make API call to fetch user information and setUser in our context. This is the part which isn't working and it keep calling our user api during navigation to each route in application.
Here is our login.js
import React, { useState, useContext } from 'react';
import { setCookies } from '../../utils/Helper';
import APIService from '../../utils/RestApiService';
import { UserContext } from '../../UserContextProvider';
import queryString from 'query-string';
import './_login.scss';
const Login = (props) => {
const [email, setEmail] = useState(null);
const [password, setPassword] = useState(null);
const [error, setError] = useState(null);
const { siteId } = props;
const { refreshToken} = useContext(UserContext);
const onKeyPress = (e) => {
if (e.which === 13) {
attemptLogin()
}
}
let params = queryString.parse(props.location.search)
let redirectTo = "/"
if (params && params.redirect)
redirectTo = params.redirect
const attemptLogin = async () => {
const payload = {
email: email,
password: password,
siteid: siteId
};
let response = await APIService.post('/login', payload);
console.log('response - ', response)
if (response.status === 200) {
const { data } = response;
setCookies('UserToken', data.token);
refreshToken(data.token)
window.location.replace(redirectTo);
}
else {
const { error } = response.data;
setError(error);
}
}
const renderErrors = () => {
return (
<div className="text-center login-error">
{error}
</div>
)
}
return (
<div className="login-parent">
<div className="container">
<div className="login-row row justify-content-center align-items-center">
<div className="login-column">
<div className="login-box">
<form className="login-form form">
<h3 className="login-form-header text-center">Login</h3>
<div className="form-group">
<label>Email:</label>
<br/>
<input
onChange={e => setEmail(e.target.value)}
placeholder="enter email address"
type="text"
onKeyPress={onKeyPress}
className="form-control"/>
</div>
<div className="form-group">
<label>Password:</label>
<br/>
<input
onChange={e => setPassword(e.target.value)}
placeholder="enter password"
type="password"
className="form-control"/>
</div>
<div className="form-group">
<button
className="btn btn-secondary btn-block"
onClick={attemptLogin}
type="button">
Login
</button>
</div>
{error ? renderErrors() : null}
</form>
</div>
</div>
</div>
</div>
</div>
)
}
export default Login;
Our userContext looks like below
import React, { useState, useEffect } from 'react'
import APIService from './utils/APIService';
import { getCookies } from './utils/Helper';
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies('UserToken'));
const [user, setUser] = useState(null);
useEffect(() => {
if (!token) return;
console.log('Inside userContext calling as token ', token)
fetchUserInfo();
}, [token]);
const fetchUserInfo = async() => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
}
/*
If user logoff or login, update token from child component
*/
const refreshToken = (newToken) => {
//token = newToken;
setToken(newToken);
fetchUserInfo()
}
return (
<UserContext.Provider value={{user, token, refreshToken}}>
{props.children}
</UserContext.Provider>
);
}
export { UserContextProvider, UserContext }
Our getCookies function which simply read cookies using universal-cookies package
export const getCookies = (name) => {
return cookies.get(name);
};
So I tried to replicate your issue using a CodeSandbox, and these are my findings based on your code:
Context:
Your context has a useEffect which depend on token. When you call refreshToken, you update the token which automatically triggers the useEffect and makes a call to fetchUserInfo. So you don't need to call fetchUserInfo after setToken in refreshToken. Your context would look like:
const UserContext = React.createContext();
const UserContextProvider = (props) => {
const [token, setToken] = useState(getCookies("UserToken"));
const [user, setUser] = useState(null);
useEffect(() => {
console.log("Inside userContext calling as token ", token);
fetchUserInfo();
}, [token]);
const fetchUserInfo = async () => {
if (token) {
let userRes = await APIService.get(`/user?token=${token}`);
console.log('User route called')
setUser(userRes.data);
}
};
const refreshToken = (newToken) => {
setToken(newToken);
};
return (
<UserContext.Provider value={{ user, token, refreshToken }}>
{props.children}
</UserContext.Provider>
);
};
export { UserContextProvider, UserContext };
Route:
Now coming to your routing, since you've not included code of AppRouter I had to make an assumption that you use react-router with Switch component. (As shown in CodeSandbox).
I see a line in your Login component which is window.location.replace(redirectTo);. When you do this, the entire page gets refreshed (reloaded?) and React triggers a re-render, which is why I suppose your context methods fire again.
Instead use the history API from react-router (Again, my assumption) like so,
let history = useHistory();
history.push(redirectTo);
Here's the sandbox if you want to play around:

How to utilize useEffect hooks to fetch data from Firebase RealTime Database

I have an issue where when my user is a new user both my create profile and create characters to FireBase Realtime database are not loading before my user profile page renders. I understand that useEffects run after the render. But after user profile and characters are created in the database I don't have the issue. I can log off and refresh my app, sign in and everything loads in time. Here is m code. I've tried writing my functions inside the useEffect several different ways and I get the same results every time. I saw one post where someone using a .then() but that doesn't appear to work in my situation. I rather not use any additional add-ins like AXIOs or other packages. I feel like there has to be a way to do this with the native built in tools of React and Firebase. Any advice is much appreciated. Edit: Here is my layout.
App.js
<AuthProvider>
<DBProvider>
<Switch>
<PrivateRoute path="/profile" component={ProfileBar} />
<PrivateRoute path="/update-profile" component={UpdateProfile} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route path="/forgot-password" component={ForgotPassword} />
</Switch>
</DBProvider>
</AuthProvider>
</Router>
AuthContext.js
import React, { useContext, useState, useEffect } from 'react'
import { auth} from '../firebase'
const AuthContext = React.createContext()
export function useAuth() {
return useContext(AuthContext)
}
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState()
const [loading, setLoading] = useState(true)
function signup(email, password, displayName) {
let promise = new Promise ((resolve, reject) => {
auth.createUserWithEmailAndPassword(email, password)
.then((ref) => {
ref.user.updateProfile({
displayName: displayName
});
resolve(ref);
})
.catch((error) => reject(error));
})
return promise
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
})
return unsubscribe
}, [])
}
DBContext.js
import { db } from '../firebase'
import { useAuth } from './AuthContext'
import React, { useState, useEffect, useContext } from 'react'
const DBContext = React.createContext() // React Database FireStore .DB
export function useDB() {
useContext(DBContext);
}
export function DBProvider({ children }) {
const [profileData, setProfileData] = useState()
const [loading, setLoading] = useState(true)
const { currentUser } = useAuth()
function checkCurrentUser(){
if(currentUser){
checkProfile()
}
if(!currentUser){
setLoading(false)
console.log("No current user logged in.")
}
}
function checkProfile(){
db.ref(`users/` + currentUser.uid + `/profile`)
.on('value', (snapshot) => {
const data = snapshot.val()
if(data == null){
console.log(data, "New user... Generating profile!")
createUserProfile()
}
if(data){
getProfile()
}
});
}
function createUserProfile(){
let profile = {}
profile.gameMaster = false
profile.editor = false
profile.email = currentUser.email
profile.displayName = currentUser.displayName
db.ref('users/' + currentUser.uid).set({
profile
}).then(() =>{
getProfile()
})
}
function getProfile(){
db.ref(`users/` + currentUser.uid + `/profile`)
.on('value', (snapshot) => {
const profile = snapshot.val()
setLoading(false)
setProfileData(profile)
console.log("Profile set to State from Database.")
})
}
useEffect(() => {
checkCurrentUser()
},[])
}
Profile.js
<Switch>
<CharacterProvider>
<Route path={`${match.path}/characters`} component={CharacterSheets} />
<Route path={`${match.path}/journal`} component={Journal} />
<Route path={`${match.path}/game_charts`} component={GameCharts} />
<Route path={`${match.path}/game_rules`} component={GameRules} />
</CharacterProvider>
</Switch>
CharacterContext.js
useEffect(() => {
const ref = db.ref(`users/` + currentUser.uid + `/characters`)
ref.on('value', snapshot => {
const data = snapshot.val()
if(data){
console.log("Setting Characters to State from Database.")
setCharacters(JSON.parse(data))
setLoading(false)
}
if(data == null){
console.log("Setting Characters to State from template.")
setCharacters(characterTemplate)
setLoading(false)
}
})
return () => ref.off();
}, [])
useEffect(() => {
if(characters){
db.ref(`users/` + currentUser.uid).child("/characters").set(JSON.stringify(characters))
}
console.log("Data saved to firebase.")
}, [characters])
CharacterCards.js
import { useCharacter } from '../../../contexts/CharacterContext'
import CharacterCard from './CharacterCard'
import CharacterCardEdit from '../../ProfileContainer/CharacterEdit/CharacterCardEdit'
import SuccessRoller from '../CharacterComponents/SuccessRoller/SuccessRoller'
export default function CharacterCards() {
const { handleCharacterAdd, characters, selectedCharacter, selectedCharacterSuccessRoller } = useCharacter()
return (
<div>
<div className="add_button-container">
<button onClick={handleCharacterAdd} className="add_button-main" >Add Character</button>
</div>
<div className="parent-container">
<div>
{characters?.map(character => {
return (
<CharacterCard key={character.id} {...character} />
)
})
}
</div>
<div>
{selectedCharacter && <CharacterCardEdit character={selectedCharacter} />}
{selectedCharacterSuccessRoller && <SuccessRoller character={selectedCharacterSuccessRoller} />}
</div>
</div>
</div>
)
}
Because your code is sharded out into many functions for readability, there are a lot of listeners that are created but don't get cleaned up. In particular great care needs to be taken with .on listeners as they may be re-fired (you could use .once() to help with this). An example of this bug is in checkProfile() which listens to the user's profile, then calls getProfile() which also listens to the profile. Each time the profile is added, another call to getProfile() is made, adding yet another listener. Plus, each of the listeners in checkProfile() and getProfile() aren't ever cleaned up.
I've made a number of assumptions about your code structure and untangled it so you can read and understand it top-to-bottom. This is especially important when working with React hooks as their order matters.
// firebase.js
import firebase from "firebase/app";
import "firebase/auth";
import "firebase/database";
firebase.initializeApp({ /* ... */ });
const auth = firebase.auth();
const db = firebase.database();
export {
firebase,
auth,
db
}
// AuthContext.js
import { auth } from "./firebase";
import React, { useContext, useEffect, useState } from "react";
const AuthContext = React.createContext();
export default AuthContext;
export function useAuth() { // <- this is an assumption
return useContext(AuthContext);
}
async function signup(email, password, avatarName) {
const userCredential = await auth.createUserWithEmailAndPassword(email, password);
await userCredential.user.updateProfile({
displayName: avatarName
});
return userCredential;
}
export function AuthProvider(props) {
const [currentUser, setCurrentUser] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => auth.onAuthStateChanged(user => {
setCurrentUser(user)
setLoading(false)
}), []);
return (
<AuthContext.Provider
value={{
currentUser,
loading,
signup
}}
>
{props.children}
</AuthContext.Provider>
);
}
// DBContext.js
import { db } from "./firebase";
import { useAuth } from "./AuthContext";
import React, { useEffect, useState } from "react";
const DBContext = React.createContext();
export default DBContext;
export function DBProvider(props) {
const [profileData, setProfileData] = useState();
const [loading, setLoading] = useState(true);
const { currentUser, loading: loadingUser } = useAuth();
useEffect(() => {
if (loadingUser) {
return; // still initializing, do nothing.
}
if (currentUser === null) {
// no user signed in!
setProfileData(null);
return;
}
// user is logged in
const profileRef = db.ref(`users/` + currentUser.uid + `/profile`);
const listener = profileRef.on('value', snapshot => {
if (!snapshot.exists()) {
// didn't find a profile for this user
snapshot.ref
.set({ // <- this will refire this listener (if successful) with the below data
gameMaster: false,
editor: false,
email: currentUser.email,
displayName: currentUser.displayName
})
.catch((error) => console.error("Failed to initialize default profile", error));
return;
}
setProfileData(snapshot.val());
setLoading(false);
});
return () => profileRef.off('value', listener); // <- cleans up listener
}, [currentUser, loadingUser]);
return (
<DBContext.Provider
value={{
profileData,
loading
}}
>
{props.children}
</DBContext.Provider>
);
}
// CharacterContext.js
import { db } from "./firebase";
import { useAuth } from "./AuthContext";
import React, { useEffect, useState } from "react";
const CharacterContext = React.createContext();
export default CharacterContext;
export function CharacterProvider(props) {
const { currentUser, loading: loadingUser } = useAuth();
const [characters, setCharacters] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
if (loadingUser) {
return; // still initializing, do nothing.
}
if (!currentUser) {
// no user signed in!
setCharacters(null);
return;
}
const charactersRef = db.ref(`users/${currentUser.uid}/characters`);
const listener = charactersRef.on('value', snapshot => {
if (!snapshot.exists()) {
// no character data found, create from template
snapshot.ref
.set(DEFAULT_CHARACTERS); // <- this will refire this listener (if successful)
.catch((error) => console.error("Failed to initialize default characters", error));
return;
}
setCharacters(JSON.parse(snapshot.val()));
setLoading(false);
});
return () => charactersRef.off('value', listener);
}, [currentUser, loadingUser]);
return (
<CharacterContext.Provider
value={{
characters,
loading
}}
>
{props.children}
</CharacterContext.Provider>
);
}

Resources