How to use useNavigate outside react hook? - reactjs

Gets list of emails from firestore and checks if current user is registered and then redirects them to sign up if they are new user.
The code is functional(it redirects succesfully) but get the following error:
arning: Cannot update a component (BrowserRouter) while rendering a different component You should call navigate() in a React.useEffect(), not when your component is first rendered.
const navigate = useNavigate();
let hasEmail = false;
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
const emailCheck = (emails) => { //checks if email exists
hasEmail = emails.some((e) => e.email === auth.currentUser.email);
};
const direct = () => { // redirects to required page
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
emailCheck(emailList);
direct();

Move the email checking logic into a useEffect hook with a dependency on the emailList state.
const navigate = useNavigate();
const [emailList, setEmailList] = useState([]);
const emailRef = collection(db, "emails");
useEffect(() => {
const getEmails = async () => {
const data = await getDocs(emailRef);
setEmailList(
data.docs.map((doc) => ({
...doc.data(),
}))
);
};
getEmails();
}, []);
useEffect(() => {
if (emailList.length) {
const hasEmail = emailList.some((e) => e.email === auth.currentUser.email);
navigate(hasEmail ? "/index" : "/enterdetails");
}
}, [auth, emailList, navigate]);

This might not run without the proper firebase config but check it out
https://codesandbox.io/s/elated-bell-kopbmp?file=/src/App.js
Things to note:
Use useMemo for hasEmail instead of emailCheck. This will re-run only when emailList changes
const hasEmail = useMemo(() => {
//checks if email exists
return emailList.some((e) => e.email === auth.currentUser.email);
}, [emailList]);
There isn't really a point in having this in a react component if you are just redirecting away. Consider having the content of 'index' at the return (</>) part of this component. Only redirect if they aren't authorized
useEffect(() => {
if (!hasEmail) {
navigate("/enterdetails");
}
//else {
// navigate("/index");
//}
}, [hasEmail, navigate]);

Related

Conditional fetching with Redux-Toolkit RTK-Query

I'm trying to migrate from createAsuncThunks to RTK-Query. On page load I check if there's no user in store then fetch user and other data from backend.
This code works well with createAsuncThunks but how it should like with RTK-Query?
const InitApp = () => {
const dispatch = useAppDispatch();
const user = useAppSelector((state) => state.user);
const ignore = useRef(false);
useEffect(() => {
if (!user && !ignore.current) {
Promise.all([
dispatch(getUser()),
dispatch(getRawMedias()),
dispatch(getFaces()),
dispatch(getProjects()),
]);
}
return () => {
ignore.current = true;
};
}, [dispatch, user]);
if (!user) {
return <Loader areaLoader />;
}
return <Outlet />;
};
I think this approach is wrong but it's not clear how to make it properly:
const {data} = useGetUserQuery()
useEffect(() => {
if (!data) {
// can't call other hooks here
}
}, [data])
Hi you can pass skip parameter, and combine it with initalState like so:
const user = useAppSelector((state) => state.user);
const { data = user } = useUserQuery(undefined, { skip: Boolean(user) });
now this would trigger only if there is no user, and data would be equal to user, if there is user

Hi, i'm retrieving data from firestore, and checking whether to direct the user to index page or to enter details for a new user But not able to do so

React code
import React, { useEffect, useState } from "react";
import { getDocs, collection } from "firebase/firestore";
import { auth, db } from "../firebase-config";
import { useNavigate } from "react-router-dom";
function Load() {
const navigate = useNavigate();
const [accountList, setAccountList] = useState([]);
const [hasEmail, setHasEmail] = useState(false);
const accountRef = collection(db, "accounts");
Am i using useEffect correctly?
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
checking whether email exists
const emailCheck = () => {
if (accountList.filter((e) => e.email === auth.currentUser.email)) {
setHasEmail(true);
} else {
setHasEmail(false);
}
};
Redirecting based on current user
const direct = () => {
if (hasEmail) {
navigate("/index");
} else {
navigate("/enterdetails");
}
};
return <div></div>;
}
The code compiles but doesn't redirect properly to any of the pages.
What changes should I make?
First question posted excuse me if format is wrong.
There are two problems here:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
emailCheck();
direct();
}, []);
In order:
Since getAccounts is asynchronous, you need to use await when calling it.
But even then, setting state is an asynchronous operation too, so the account list won't be updated immediately after getAccounts completes - even when you use await when calling it.
If you don't use the accountList for rendering UI, you should probably get rid of it as a useState hook altogether, and just use regular JavaScript variables to pass the value around.
But even if you use it in the UI, you'll need to use different logic to check its results. For example, you could run the extra checks inside the getAccounts function and have them use the same results as a regular variable:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
const result = data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}));
setAccountList(result);
emailCheck(result);
direct();
};
getAccounts();
}, []);
const emailCheck = (accounts) => {
setHasEmail(accounts.some((e) => e.email === auth.currentUser.email));
};
Alternatively, you can use a second effect that depends on the accountList state variable to perform the check and redirect:
useEffect(() => {
const getAccounts = async () => {
const data = await getDocs(accountRef);
setAccountList(
data.docs.map((doc) => ({
...doc.data(),
id: doc.id,
}))
);
};
getAccounts();
});
useEffect(() => {
emailCheck();
direct();
}, [accountList]);
Now the second effect will be triggered each time the accountList is updated in the state.

UseEffect is not called

I have a question about useEffect. My useEffect is not fetching the data the first time, I have to switch route for it to have the data I needed
const Comments = ({ ...rest }) => {
const theme = useTheme();
const classes = useStyles({ theme });
const [users, setUsers] = useState([]);
const { push } = useHistory();
const { token, loading } = useContext(AuthContext)
const dispatch = useDispatch();
const allUsers = useSelector(state => state.allUsers);
const comments = useSelector(state => state.listCommentsByBookId);
const listBooks = useSelector((state) => state.userListBooks);
const isFetching = useSelector((state) => state.isFetching);
const [stateReady, setReadyForRender] = useState(false)
const redirectTo = ( rowData ) => {
push({
pathname: ROUTE.USERS_DETAILS,
user: rowData
});
}
const options = {
filterType: 'checkbox',
selectableRowsHeader: false,
selectableRowsHideCheckboxes: false,
selectableRowsOnClick: false,
onRowClick: redirectTo,
};
const getAllComments = async () => {
var allusersId = [];
//get all ids
await allUsers.map((user) => {
allusersId.push(user.uid);
})
//get all books from users
await allusersId.map(async (id) => {
await dispatch(getUserListBooks(apiURL + `api/bdd/userListBooks/${id}`, token))
})
var listArray = [];
//filter the array and delete empty rows
listArray.push(listBooks);
var newArray = listArray.filter(e => e);
//map every user and stock the list of books in string
await newArray.forEach(async (book)=> {
await book.map(async (book) => {
await dispatch(getCommentsByBookId(apiURL + `api/bdd/ratingByBook/${book.id}`, token));
})
})
setReadyForRender(true)
}
useEffect(() => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
}, [stateReady])
console.log('COM', comments);
return (
<div>
{stateReady &&
<Card>
<Box className={classes.tableContainer} sx={{ minWidth: 1050 }}>
<MUIDataTable
data={comments}
columns={columns}
options={options}
/>
</Box>
</Card>}
</div>
);
};
Why? It might be related to async await but I'm stuck here.
If you want to fetch these informations on the first render, you'll have to pass an empty array as the second parameter of your useEffect.
The reason your useEffect is not called is because stateReady does not change during the course of your current code.
See this link, particularly the note section, it explains way better than me how the empty array as second parameter works.
Can you replace the useEffect section to the below code:
useEffect(() => {
(async () => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
})()
}, [stateReady])
You can read more about this in this link
You can use eslint to show errors when coding with hooks. In this case if you want useEffect to handle stateReady, please provide it in the function getAllComments() => getAllComments(stateReady) and when you call this function in useEffect with [stateReady] as dependencies, it'll work.
You should remove stateReady from your dependency array in the useEffect hook. Adding variables in the dependency array means that the use Effect hooks fires only when one of the dependencies changes. Here's how to use useEffect as lifecycle methods https://reactjs.org/docs/hooks-effect.html
useEffect(() => {
console.log('is fetching', isFetching)
if(comments.length === 0) {
getAllComments();
}
});

Firebase + react : read document in auth state changed and add it to context

Based on https://dev.to/bmcmahen/using-firebase-with-react-hooks-21ap I have a authentication hook to get user state and firestore hook to get user data.
export const useAuth = () => {
const [state, setState] = React.useState(() => { const user = firebase.auth().currentUser return { initializing: !user, user, } })
function onChange(user) {
setState({ initializing: false, user })
}
React.useEffect(() => {
// listen for auth state changes
const unsubscribe = firebase.auth().onAuthStateChanged(onChange)
// unsubscribe to the listener when unmounting
return () => unsubscribe()
}, [])
return state
}
function useIngredients(id) {
const [error, setError] = React.useState(false)
const [loading, setLoading] = React.useState(true)
const [ingredients, setIngredients] = React.useState([])
useEffect(
() => {
const unsubscribe = firebase
.firestore()
.collection('recipes')
.doc(id)
.collection('ingredients') .onSnapshot( snapshot => { const ingredients = [] snapshot.forEach(doc => { ingredients.push(doc) }) setLoading(false) setIngredients(ingredients) }, err => { setError(err) } )
return () => unsubscribe()
},
[id]
)
return {
error,
loading,
ingredients,
}
}
Now in my app I can use this to get user state and data
function App() {
const { initializing, user } = useAuth()
const [error,loading,ingredients,] = useIngredients(user.uid);
if (initializing) {
return <div>Loading</div>
}
return (
<userContext.Provider value={{ user }}> <UserProfile /> </userContext.Provider> )
}
Since UID is null before auth state change trigger, firebase hook is getting called with empty key.
How to fetch data in this scenario once we understand that user is logged in.
May be you can add your document read inside auth hook.
export const useAuth = () => {
const [userContext, setUserContext] = useState<UserContext>(() => {
const context: UserContext = {
isAuthenticated: false,
isInitialized: false,
user: auth.currentUser,
userDetails: undefined
};
return context;
})
function onChange (user: firebase.User | null) {
if (user) {
db.collection('CollectionName').doc(user.uid)
.get()
.then(function (doc) {
//set it to context
})
});
}
}
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(onChange)
return () => unsubscribe()
}, [])
return userContextState
}
You can use some loading spinner in your provider to wait for things to complete.

What's the best way to do request multiple data from Firebase in React

I was wondering what would be the best way to get multiple data from Firebase in an async function to wait for some data from the first request. I'm using this code right now but it's not reliable and it breaks sometimes saying that it can't fetch the data for the second call as it's undefined.
function useOccasion() {
const [occasionData, setOccasionData] = useState(null)
const [friend, setFriend] = useState(null)
let { occasion } = useParams()
useEffect(() => {
const unsubscribe = firestore.collection('occasions').doc(occasion)
.onSnapshot(async eventData => {
setOccasionData({id: eventData.id, ...eventData.data()})
let friendData = await firestore.collection("friends").doc(eventData.data().friend).get();
setFriend({id: friendData.id, ...friendData.data()});
})
return () => unsubscribe()
}, [occasion])
return [occasionData, friend]
}
If there's a more robust way to achieve this that would be amazing.
I'd suggest breaking your data fetches into two hooks, one for each collection occasion and friends.
I'm not sure how you've set firebase up, but I access it through context.
e.g.
const useOccasion = () => {
const firebase = useContext(FirebaseContext)
const [occasions, setOccasions] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
const unsubscribe = firebase.db.collection('occasions')
.onSnapshot(snapshot => {
if (snapshot.size) {
let occasionList = []
snapshot.forEach(doc =>
occasionList.push({ ...doc.data(), uid: doc.id }),
)
setOccasions(occasionList)
setLoading(false)
} else {
setOccasions([])
setLoading(false)
}
})
return () => {
unsubscribe()
}
}, [])
return { occasions, loading }
}
Finally, in your component where you require the data you can access this hook:
const { occasions, loading } = useOccasion()

Resources