I have the following code that grabs user from my firestore database and then returns some components based on some user logic, but because the call to the database is asynchronous, the logic will run before my user object is grabbed. Is there any way I can tell the if-statements to run after user is grabbed with my useEffect().
For example, console.log(user.isSeller) initially is undefined and then turns into true once the data is recieved.
function StorefrontPage(props) {
const auth = useAuth();
const router = useRouter();
const [user, setUser] = useState({});
const uid = auth.user && auth.user.uid;
useEffect(() => {
uid && getUser(uid).then(currentUser => {
setUser(currentUser);
})
}, [auth])
console.log(user.isSeller);
//logic with user object
if (uid) {
if (user.isSeller) {
if (props.sellerId === uid) {
return (
<div>
<Storefront sellerId={props.sellerId}></Storefront>
<style jsx>{
"background-image:linear-gradient(to bottom, #ffffff, #D0D8FD);"
}</style>
</div>
);
}
else {
return (
<div>
<PublicStorefront sellerId={props.sellerId}></PublicStorefront>
<style jsx>{
"background-image:linear-gradient(to bottom, #ffffff, #D0D8FD);"
}</style>
</div>
)
}
}
else {
router.push(`/buyersportal/${auth.user.uid}`)
return (
<div>
<p>
Sending you to the buyer's Portal.
</p>
</div>
)
}
}
else {
return (
<div>
<PublicStorefront sellerId={props.sellerId}></PublicStorefront>
</div>
)
}
}
export default StorefrontPage;
Any idea if there are some functions I can use to make it wait until user is called?
Typically the pattern to solve this is with an isLoading state
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
setIsLoading(true); // set loading to true again in case auth gets updated
uid && getUser(uid).then(currentUser => {
setUser(currentUser);
setIsLoading(false);
})
}, [auth])
then you can do conditional rendering like so:
if (isLoading) return <LoadingSpinner/>
However if auth never gets updated you can also initialise user to be null:
const [user, setUser] = useState(null);
and then just check if the user exists or not with
if (user === null) return <LoadingSpinner/>
Set user as 'undefined' or 'null' and Just return a loader (or whatever you want as long as it is jsx) while you fetch your data :
if(!user){
return <Loader />
}
// your logic if condition above isn't matched
When user will be updated your component will be re-rendered
Related
I want to use react-firebase-hooks for firestore, but I cant seem to make it work properly, I have collection of users and it has subcollection of budgets. If user is logged in I want to show his budgets. My code:
The problem is mainly in useCollection hook, where I need to pass user.uid, but user is undefined in the beginning. How else should I do this?
const Homepage = () => {
const [user, loading, error] = useAuthState(auth);
const [showAddBudgetModal, setShowAddBudgetModal] = useState(false);
const [value, dataLoading, dataError] = useCollection(collection(db, `users/${user!.uid}/budgets`), {
snapshotListenOptions: { includeMetadataChanges: true },
});
const [showAddExpenseModal, setShowAddExpenseModal] = useState(false);
const [addExpenseModalBudgetId, setAddExpenseModalBudgetId] = useState('');
const openAddExpenseModal = (budgetId?: any) => {
setShowAddExpenseModal(true);
setAddExpenseModalBudgetId(budgetId);
};
if (loading) {
return (
<div>
<p>Initialising User...</p>
</div>
);
}
if (error) {
return (
<div>
<p>Error: {error.message}</p>
</div>
);
}
if (user) {
return (
<div>
<h1>Welcome {user.displayName}!</h1>
<button onClick={logOut}>Logout</button>
<div className="buttons">
<button onClick={() => setShowAddBudgetModal(true)}>Add budget</button>
</div>
<AddBudgetModal show={showAddBudgetModal} onClose={() => setShowAddBudgetModal(false)}></AddBudgetModal>
<div>
{dataError && <strong>Error: {JSON.stringify(error)}</strong>}
{dataLoading && <span>Collection: Loading...</span>}
{value && user && (
<div>
{value.docs.map((budget) => (
<BudgetCard
key={budget.id}
name={budget.name}
max={budget.max}
onAddExpenseClick={() => openAddExpenseModal(budget.id)}
onViewExpensesClick={() => openAddExpenseModal(budget.id)}
></BudgetCard>
))}
</div>
)}
</div>
</div>
);
}
};
export default Homepage;
There are 2 ways to solve this problem in my opinion
First one is to use optional chaining to check whether the user exist or not eg:
const [value, dataLoading, dataError] = useCollection(user && query(
collection(getFirestore(app), "users", user.uid, "budgets"), {
snapshotListenOptions: {
includeMetadataChanges: true
},
});
For more information about this there is a similar thread about this issue.
Second way is to use useEffect hook to implement the same like this:
useEffect(() => {
if (user) {
const [value, dataLoading, dataError] = useCollection(user && query(
collection(getFirestore(app), "users", user.uid, "budgets"), {
snapshotListenOptions: {
includeMetadataChanges: true
},
});
}
}, [user]);
In this way your user can only be rendered if and only if data related to the user is loaded already, but make sure in future to not set user in useEffect as it will create an infinite loop as useEffect has provided user in dependencies array.
For more about usEffect you can go through this documentations
I am new to React and I tried to toggle the Login/Logout based on the current state of authentication. I've used Google OAuth to perform the authentication.
I have a state variable to say if the user is authenticated or not and is defaulted to false. Upon successful authentication, I set the state to true.
Now the problem is, after completing a successful authentication, when I refresh the screen, the screen reloads and I see the console.log printing false and login appears momentarily. And after a second the console.log prints true and then the logout appears. How do I avoid showing login screen (for that one second after refreshing the screen) when the authentication is completed? Can someone help me please? Thanks.
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
setIsAuthenticated(false)
}, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}
If your variable goes from false to true it means your code is doing something, probably an AJAX call, my recommendation is to show a loading scree/message until the AJAX request is completed.
There is no way to keep the variable intact on reload but you can keep a variable that tracks if the user authentication has been initialized and show a loading indicator in the meantime
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [authLoaded, setAuthLoaded] = useState(false)
// this does nothing, passing false to useState above sets the initial value
// useEffect(() => {
// setIsAuthenticated(false)
// }, [])
const handleSuccessAuth = x => {
setIsAuthenticated(true)
setAuthLoaded(true)
}
const handleFailureAuth = x => {
setIsAuthenticated(false)
setAuthLoaded(true)
}
const handleLogout = x => {
setIsAuthenticated(false)
}
console.log(isAuthenticated)
if (!authLoaded) {
return <div>loading...</div>
}
if(!isAuthenticated)
{
return (
<div>
<LoginView
handleSuccessAuth = {handleSuccessAuth}
handleFailureAuth = {handleFailureAuth}
/>
</div>
)
}
else
{
return (
<div>
<LogoutView
handleLogout = {handleLogout}
/>
</div>)
}
I believe olivier-boisse alluded to using localStorage to persist your state. You can use an useEffect hook to persist your isAuthenticated to localStorage when the value updates, and use a state initializer function to read in the initial state from local storage.
const [isAuthenticated, setIsAuthenticated] = useState(() => {
return !!JSON.parse(localStorage.getItem('auth');
});
useEffect(() => {
localStorage.setItem('auth', JSON.stringify(isAuthenticated));
}, [isAuthenticated]);
I have (what seems to be) a very peculiar situation where I seem to be getting extra events emitted based on my Redux state.
I have narrowed the behavior down to whether or not I make a successful request to my /users endpoint and retrieve a list of users which is then stored in Redux.
If the commented code is not active (as it is currently shown), I am able to successfully render the modal(s) reliably and step between states.
If the commented code is active, the (which is what is behind the as well) emits an onDismiss call immediately. This has the result of closing the modal immediately.
If the commented code is active, but the response from the thunk is a 401 and the user data is not loaded (i.e., the state of the user key in redux is a failure, not success, then the modal works -- though of course, there are no users to select.
I have confirmed this behavior is consistent no matter where I seem to make this fetch request (initially it was in the App.tsx to be called immediately. I also tried it in an intermediate component).
Question(s):
Can you explain why I might be getting different behavior in my click handlers based on what is in my state?
Is there something I'm missing and I'm conflating my Redux state with the actual behavior?
I know I can solve this by adding a event.stopPropagation() call in strategic places (e.g., on the first button that opens the <ConfirmationBox> and then again on the button in the <ConfirmationBox> that transitions to the SelectUser modal), but are there other solutions?
//pinFlow.tsx
type States =
| { state: 'Confirm' }
| { state: 'SelectUser' }
| { state: 'SubmitPin'; user: User };
export function pinFlow<T extends ConfirmationBoxProps>(
ConfirmationBox: React.FC<T>,
authorization: Authorization,
) {
const [state, setState] = React.useState<States>({ state: 'Confirm' });
// const dispatch=useDispatch();
// initialize users
// const users = useSelector((state: InitialState) => state.pinAuth.users);
// const fetchUsers = useCallback(() => {
// dispatch(fetchUsersThunk());
// }, [dispatch]);
// useEffect(() => {
// if (users.state === RemoteDataState.NotStarted) {
// fetchUsers();
// }
// }, [fetchUsers, users.state]);
return (props: T) => {
const users = useSelector((state: InitialState) =>
mapRemoteData(state.pinAuth.users, users =>
users.filter(user => user.authorizations.includes(authorization)),
),
);
switch (state.state) {
case 'Confirm': {
return (
<ConfirmationBox
{...props}
onSubmit={(_event: React.MouseEvent) => {
setState({ state: 'SelectUser' });
}}
/>
);
}
case 'SelectUser': {
return (
<Modal
title={'PIN Required'}
canClickOutsideToDismiss={true}
onDismiss={() => {
setState({ state: 'Confirm' });
}}
>
<p className={style.selectProfileText}>Select your profile:</p>
<pre>
<code>{JSON.stringify(users, null, 4)}</code>
</pre>
{/*
<UserList users={users.data} /> */}
</Modal>
);
}
default: {
return <Modal title="others">all others</Modal>;
}
}
};
}
The code is used in another component like so:
function Comp(){
const [selected, setSelected] = useState();
const [mode, setMode] = useState();
const ConfirmationModal =
protected
? pinFlow(MenuItemModal, permission)
: MenuItemModal;
return(
<ConfirmationModal
item={selected}
mode={mode}
disabled={availability.state === RemoteDataState.Loading}
errorMessage={tryGetError(availability)}
onCancel={() => {
setMode(undefined);
dispatch(resetAvailability());
}}
onSubmit={(accessToken: string) => {
dispatch(findAction(selected, mode, accessToken));
}}
/>
)
}
I made a search bar that allows the user to search all sports available in one specific city (if sport is not defined) or a specific sport in a specific city (if sport is defined).
City will allways be defined.
I put 2 inputs (city and sport) on my searchbar and I want immediate results (so that there is a request to my API without any button "search" that triggers the request).
So when the user types something on the city input it triggers a request to the API and when he types something on the sport input it retriggers the request but this time with the city and the sport defined.
Both inputs values are store in states (city and sport).
I manage to do something that seems to work, the only problem is that if I types a sport in my input search, it does not update my request to the API. I have to retype the sport in my input a second time so that the request is updated.
I don't know why it does not update the first time I types something in my sport input because I have specified on my useEffect array that it must re render when the sport state changes.
Can someone help me understand this ?
My code :
import React, { useState, useEffect } from "react";
import style from "../styles/pdrs.module.css";
import axios from "axios";
import SearchBar from "../components/SearchBar";
const Pdrs = ({ setSearchCity, searchSport, setSearchSport }) => {
// if request's result is loading
const [isLoading, setIsLoading] = useState(false);
// search result
const [searchresults, setSearchresults] = useState(
"Lancez une recherche avec au moins une ville !"
);
// state for the searchbar request
const [city, setCity] = useState("");
const [sport, setSport] = useState(0);
// get city's id for API's request
const fetchCity = async () => {
setIsLoading(true);
try {
// city search
const cityResponse = await axios.get(
`${baseAPI}/city/name=${searchCity}`
);
const city = cityResponse.data;
setCity(city);
setIsLoading(false);
} catch (error) {
console.log(error.message);
setIsLoading(false);
}
};
//fetching sport id
const fetchSport = async () => {
setIsLoading(true);
try {
const sportResponse = await axios.get(
`${baseAPI}/activity/name=${searchSport}`
);
setSport(sportResponse.data.data[0].macro_activity_id);
setIsLoading(false);
} catch (error) {
console.log(error.message);
}
};
//fetching final request response
const fetchDataRequest = async () => {
try {
setIsLoading(true);
const results = await axios.get(
`${baseAPI}/pdrs?city_id=${city.id}${
sport ? "¯o_activity_id=" + sport : ""
}`
);
// manage search results
if (results.data.nb_results === 1) {
setSearchresults({
data: [results.data.data],
nb_results: 1,
});
setNbResults(1);
setIsLoading(false);
} else {
setSearchresults(results.data);
setNbResults(results.data.nb_results);
setIsLoading(false);
}
} catch (error) {
console.log(error.message);
setSearchresults(
"Sorry, nothing was found... !"
);
}
};
useEffect(() => {
if (searchCity) {
fetchCity();
}
if (searchSport) {
fetchSport();
}
}, [searchCity, searchSport]);
useEffect(() => {
if (searchCity) {
fetchDataRequest();
}
}, [searchCity, searchSport]);
console.log(searchresults);
return <>
<main className={`container ${style.pdrs}`}>
<section className={style.searchbar}>
<SearchBar
searchCity={searchCity}
setSearchCity={setSearchCity}
searchSport={searchSport}
setSearchSport={setSearchSport}
searchInstallation={searchInstallation}
setSearchInstallation={setSearchInstallation}
searchType={searchType}
setSearchType={setSearchType}
setPage={setPage}
/>
</section>
<section className={style.results}>
{isLoading ? (
<div>Loading...</div>
) : typeof searchresults === "string" ? (
<div className={`${style.container} ${style.noResults}`}>
<h2>{searchresults}</h2>
</div>
) : (
<>
<div className={style.container}>
<div className={style.resultsList}>
{searchresults.data.map((pdrs) => {
return (
// some code displaying the result
);
})}
</div>
</div>
</>
)}
</section>
</main>
</>;
};
export default Pdrs;
Since you are having two useEffect and one is setting city and sport you would need to make debounce for making a call for fetching list by itself.
I would suggest that you firstly make changes to your use effect for API call fetchDataRequest:
useEffect(() => {
if (searchCity) {
fetchDataRequest();
}
}, [city, sport]);
You would listen to the actual data from BE, not from input that you fill.
And secondly you can use library useDebounce from here https://www.npmjs.com/package/use-debounce and use useDebounceCallback to delay calling API call after you select sport/city.
I am creating a React.js app which got 2 components - The main one is a container for the 2nd and is responsible for retrieving the information from a web api and then pass it to the child component which is responsible for displaying the info in a list of items. The displaying component is supposed to present a loading spinner while waiting for the data items from the parent component.
The problem is that when the app is loaded, I first get an empty list of items and then all of a sudden all the info is loaded to the list, without the spinner ever showing. I get a filter first in one of the useEffects and based on that info, I am bringing the items themselves.
The parent is doing something like this:
useEffect(() =>
{
async function getNames()
{
setIsLoading(true);
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
useEffect(() =>
{
async function getItems()
{
setIsLoading(true);
const items= await WebAPI.getItems(selectedName);
setAllItems(items);
setIsLoading(false);
};
getTenants();
},[selectedName]);
.
.
.
return (
<DisplayItems items={allItems} isLoading={isLoading} />
);
And the child components is doing something like this:
let spinner = props.isLoading ? <Spinner className="SpinnerStyle" /> : null; //please assume no issues in Spinner component
let items = props.items;
return (
{spinner}
{items}
)
I'm guessing that the problem is that the setEffect is asynchronous which is why the component is first loaded with isLoading false and then the entire action of setEffect is invoked before actually changing the state? Since I do assume here that I first set the isLoading and then there's a rerender and then we continue to the rest of the code on useEffect. I'm not sure how to do it correctly
The problem was with the asynchronicity when using mulitple useEffect. What I did to solve the issue was adding another spinner for the filters values I mentioned, and then the useEffect responsible for retrieving the values set is loading for that specific spinner, while the other for retrieving the items themselves set the isLoading for the main spinner of the items.
instead of doing it like you are I would slightly tweak it:
remove setIsLoading(true); from below
useEffect(() =>
{
async function getNames()
{
setIsLoading(true); //REMOVE THIS LINE
const names = await WebAPI.getNames();
setAllNames(names);
setSelectedName(names[0]);
setIsLoading(false);
};
getNames();
} ,[]);
and have isLoading set to true in your initial state. that way, it's always going to show loading until you explicitly tell it not to. i.e. when you have got your data
also change the rendering to this:
let items = props.items;
return isLoading ? (
<Spinner className="SpinnerStyle" />
) : <div> {items} </div>
this is full example with loading :
const fakeApi = (name) =>
new Promise((resolve)=> {
setTimeout(() => {
resolve([{ name: "Mike", id: 1 }, { name: "James", id: 2 }].filter(item=>item.name===name));
}, 3000);
})
const getName =()=> new Promise((resolve)=> {
setTimeout(() => {
resolve("Mike");
}, 3000);
})
const Parent = () => {
const [name, setName] = React.useState();
const [data, setData] = React.useState();
const [loading, setLoading] = React.useState(false);
const fetchData =(name) =>{
if(!loading) setLoading(true);
fakeApi(name).then(res=>
setData(res)
)
}
const fetchName = ()=>{
setLoading(true);
getName().then(res=> setName(res))
}
React.useEffect(() => {
fetchName();
}, []);
React.useEffect(() => {
if(name)fetchData(name);
}, [name]);
React.useEffect(() => {
if(data && loading) setLoading(false)
}, [data]);
return (
<div>
{loading
? "Loading..."
: data && data.map((d)=>(<Child key={d.id} {...d} />))}
</div>
);
};
const Child = ({ name,id }) =>(<div>{name} {id}</div>)
ReactDOM.render(<Parent/>,document.getElementById("root"))
<script crossorigin src="https://unpkg.com/react#16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#16/umd/react-dom.production.min.js"></script>
<div id="root"></div>