How to stop multiple calls of the same query using React-query? - reactjs

I am using React-query for my React app.
I have useLogin and useLogout hooks which use useQuery:
export const useLogin = (input?: LoginWithEmailPassword) => {
const { data, isLoading, isSuccess, error } = useQuery(
["loginWithEmailPassword"],
() => loginWithEmailPassword(input),
{
enabled: !!input,
}
);
return { data: data?.data, isLoading, isSuccess, error };
};
export const useLogout = (accessToken?: string) => {
const { isSuccess, isLoading, error } = useQuery(
["logout"],
() => logout(accessToken),
{
enabled: !!accessToken,
}
);
return { isSuccess, isLoading, error };
};
In my AuthProvider; where I use the 2 hooks, I also have a login and logout function which will be called when a user clicks login/logout.
Given React-query's declarative approach, I'm also keeping 2 states; loginInput and logoutInput
AuthProvider.tsx
const [loginInput, setLoginInput] = useState<LoginWithEmailPassword>();
const [logoutInput, setLogoutInput] = useState<string | undefined>();
const { data, isSuccess: isLoginSuccess, isLoading } = useLogin(loginInput);
const {...} = useLogout(logoutInput);
const login = (input: LoginWithEmailPassword) => {
setLoginInput(input);
};
const logout = () => {
setLogoutInput(data?.accessToken);
};
This issue I've found is that after the user clicks login and set's the loginInput state; useLogin will run. But useLogin will re-run every time the component re-rerenders because loginInput will still have the state; i.e. if a user clicks logout, useLogin will run again. What would be the best way to resolve this?
Things will be more straightforward if React-query has a useLazyQuery like Apollo.
A hacky approach I can think of is to reset the loginInput state to undefined, like so:
useEffect(()=>{
if(isLoginSuccess && !isLoading && !!data) setLoginInput(undefined)
},[isLoginSuccess])

login and logout are not queries, but mutations. They are not idempotent - you can't run them at will. They change some state on the server (logging the user in) or create a resource (a login token).
So, the answer is: don't use queries here.

Related

Is there a way to know whether or not onAuthStateChanged has run? Preventing Login Flash

While using firebase 0auth, react, and redux, I run into a pattern where I do not know whether or not a user is logged in for some time during the first fire of onAuthStateChanged, meaning that my login screen flickers.
The solution to this question may be purely react or redux, but it also may very well lie in the firebase 0auth implementation. Apologies if I'm in the wrong place.
Is there a pattern here that is incorrect or something that firebase recommends to handle this type of thing? Ideally, a logged in user would immediately pass through the login component, but I know this is an asynchronous operation. Alternatively, I could set a loading state, but then I'd need to know when onAuthStateChanged has ran once. Is there a way to know that? I could also just simply do a setTimeout to cover for the fetch, but I really don't want to do that. It seems cheap and won't cover me in all cases.
Here's the general setup of my implementation:
Login (wrapping the entire app):
export default function Login(props: LoginProps) {
const { children } = props;
const dispatch = useAppDispatch();
const user = useAppSelector(selectUser);
const provider = new GoogleAuthProvider();
const auth = getAuth();
useDeviceLanguage(auth);
useEffect(() => {
dispatch(startUsers());
}, [dispatch]);
return !user ? (
// ...
<Button
variant="contained"
fullWidth
onClick={() => {
signInWithPopup(auth, provider)
.then((result) => {
const credential =
GoogleAuthProvider.credentialFromResult(result);
const token = credential?.accessToken;
const user = result.user;
})
.catch((error) => {
const errorCode = error.code;
const errorMessage = error.message;
const credential =
GoogleAuthProvider.credentialFromError(error);
});
}}
>
Sign in
</Button>
// ...
) : (
<div>{children}</div>
);
}
dispatch(startUsers()) kicks off my redux middleware to listen for users:
userListener
export const userListenerMiddleware = createListenerMiddleware();
userListenerMiddleware.startListening({
actionCreator: startUsers,
effect: async (action, listenerApi) => {
listenerApi.cancelActiveListeners();
const auth = getAuth();
onAuthStateChanged(auth, (user) => {
if (user) {
listenerApi.dispatch(signedInUser(user));
} else {
listenerApi.dispatch(signedOutUser());
}
});
},
});
The user slice:
interface UserState {
user: User | undefined;
}
const initialState: UserState = {
user: undefined,
};
export const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
startUsers: () => initialState,
signedInUser: (state, action: PayloadAction<User>) => {
state.user = action.payload;
},
signedOutUser: (state) => {
state.user = undefined;
},
},
});
I completely understand this may not be a firebase thing, but it certainly has the firebase documentation pattern at its core. I basically just read down the docs to create this scheme.
How can I ensure my login screen doesn't flicker when a user is signed in? I would fully accept putting a spinner in its place, but I would be averse to basing it on a timer.

How to use react hook in specific NextJS? (Firebase Authentication)

I have a nextjs app. I want to authenticate users with firebase. But I want some pages to client-side render and some pages to server-side render. But When I am using this Hook in _app.tsx all pages are rendered on the client side.
How can I use this hook on a specific page so that only that page renders on the client side?
_app.tsx
function MyApp({ Component, pageProps }: AppProps) {
return (
<UserAuthContentProvider>
<Layout>
<Component {...pageProps} />
</Layout></UserAuthContentProvider>
);
}
AuthContext Hook
export const auth = getAuth(app);
const AuthContext = createContext<any>({});
export const useAuthContextProvider = () => useContext(AuthContext);
export const UserAuthContentProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const router = useRouter();
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [isUserAuthenticated, setIsUserAuthenticated] = useState(false);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (user) => {
if (user) {
setUser(user);
setIsUserAuthenticated(true);
} else {
setUser(null);
setIsUserAuthenticated(false);
}
setLoading(false);
});
return () => unsubscribe();
});
};
const signUp = async (email: string, password: string) => {
await createUserWithEmailAndPassword(auth, email, password).then((result) => {
if (!result.user.emailVerified) {
router.push("/verification");
} else {
router.push("/dashboard");
}
});
};
const logIn = async (email: string, password: string) => {
await signInWithEmailAndPassword(auth, email, password).then((result) => {
if (!result.user.emailVerified) {
router.push("/verification");
} else {
router.push("/dashboard");
}
});
};
const logOut = async () => {
setUser(null);
await auth.signOut().finally(() => {
router.push("/");
});
};
return (
<AuthContext.Provider
value={{
user,
logIn,
signUp,
logOut,
isUserAuthenticated,
}}
>
{loading ? null : children}
</AuthContext.Provider>
);
If you look in the NextJS Data Fetching docs here they go over which hook to use to trigger which pages you want to render and where.
If you want certain pages to server render you use getServerSideProps if you want to do a regular React runtime client render you can use getInitialProps and if you want to Static Site Generate at server build time you use getStaticProps. Depending on which hook you use on which Page is what determines the NextJS rendering strategy.
In development mode NextJS always uses SSR for developer experience so if you want to test you will need to run npm run build && npm run start.
If you only want a certain page to do one of the rending strategies maybe you can put the rending hook with the strategy you want as a noop on that page.
Since that hook is in the _app it will always run during all strategies so the pages always have that data hydrated. Depending on the strategy will depend on how often that data updates or when its referenced during the build cycle.

How to GET data in a custom hook and dispatch it to context

I'm fairly new to the context API and react hooks beyond useState and useEffect so please bare with me.
I'm trying to create a custom useGet hook that I can use to GET some data from the backend then store this using the context API, so that if I useGet again elsewhere in the app with the same context, it can first check to see if the data has been retrieved and save some time and resources having to do another GET request. I'm trying to write it to be used generally with various different data and context.
I've got most of it working up until I come to try and dispatch the data to useReducer state and then I get the error:
Hooks can only be called inside the body of a function component.
I know I'm probably breaking the rules of hooks with my call to dispatch, but I don't understand why only one of my calls throws the error, or how to fix it to do what I need. Any help would be greatly appreciated.
commandsContext.js
import React, { useReducer, useContext } from "react";
const CommandsState = React.createContext({});
const CommandsDispatch = React.createContext(null);
function CommandsContextProvider({ children }) {
const [state, dispatch] = useReducer({});
return (
<CommandsState.Provider value={state}>
<CommandsDispatch.Provider value={dispatch}>
{children}
</CommandsDispatch.Provider>
</CommandsState.Provider>
);
}
function useCommandsState() {
const context = useContext(CommandsState);
if (context === undefined) {
throw new Error("Must be within CommandsState.Provider");
}
return context;
}
function useCommandsDispatch() {
const context = useContext(CommandsDispatch);
if (context === undefined) {
throw new Error("Must be within CommandsDispatch.Provider");
}
return context;
}
export { CommandsContextProvider, useCommandsState, useCommandsDispatch };
useGet.js
import { API } from "aws-amplify";
import { useRef, useEffect, useReducer } from "react";
export default function useGet(url, useContextState, useContextDispatch) {
const stateRef = useRef(useContextState);
const dispatchRef = useRef(useContextDispatch);
const initialState = {
status: "idle",
error: null,
data: [],
};
const [state, dispatch] = useReducer((state, action) => {
switch (action.type) {
case "FETCHING":
return { ...initialState, status: "fetching" };
case "FETCHED":
return { ...initialState, status: "fetched", data: action.payload };
case "ERROR":
return { ...initialState, status: "error", error: action.payload };
default:
return state;
}
}, initialState);
useEffect(() => {
if (!url) return;
const getData = async () => {
dispatch({ type: "FETCHING" });
if (stateRef.current[url]) { // < Why doesn't this also cause an error
const data = stateRef.current[url];
dispatch({ type: "FETCHED", payload: data });
} else {
try {
const response = await API.get("talkbackBE", url);
dispatchRef.current({ url: response }); // < This causes the error
dispatch({ type: "FETCHED", payload: response });
} catch (error) {
dispatch({ type: "ERROR", payload: error.message });
}
}
};
getData();
}, [url]);
return state;
}
EDIT --
useCommandsState and useCommandsDispatch are imported to this component where I call useGet passing the down.
import {
useCommandsState,
useCommandsDispatch,
} from "../../contexts/commandsContext.js";
export default function General({ userId }) {
const commands = useGet(
"/commands?userId=" + userId,
useCommandsState,
useCommandsDispatch
);
Why am I only getting an error for the dispatchRef.current, and not the stateRef.current, When they both do exactly the same thing for the state/dispatch of useReducer?
How can I refactor this to solve my problem? To summarise, I need to be able to call useGet in two or more places for each context with the first time it's called the data being stored in the context passed.
Here are various links to things I have been reading, which have helped me to get this far.
How to combine custom hook for data fetching and context?
Updating useReducer 'state' using useEffect
Accessing context from useEffect
https://reactjs.org/warnings/invalid-hook-call-warning.html
I think your problem is because you are using useRef instead of state for storing state. If you useRef for storing state you need to manually tell react to update.
I personally would not use reducer and just stick to the hooks you are familiar with as they fulfill your current requirements. I also think they are the best tools for this simple task and are easier to follow.
Code
useGetFromApi.js
This is a generalized and reusable hook - can be used inside and outside of the context
export const useGetFromApi = (url) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!url) return;
const getData = async () => {
try {
setLoading(true);
setData(await API.get('talkbackBE', url));
} catch ({ message }) {
setError(message);
} finally {
setLoading(false); // always set loading to false
}
};
getData();
}, [url]);
return { data, error, loading };
};
dataProvider.js
export const DataContext = createContext(null);
export const DataProvider = ({ children, url}) => {
const { data, error, loading } = useGetFromApi(url);
return (
<DataContext.Provider value={{ data, error, loading }}>
{children}
</DataContext.Provider>
);
};
useGet.js
Don't need to check if context is undefined - React will let you know
export const useGet = () => useContext(DataContext);
Usage
Most parent wrapping component that needs access to data. This level doesn't have access to the data - only it's children do!
const PageorLayout = ({children}) => (
<DataProvider url="">{children}</DataProvider>
)
A page or component that is nested inside of the context
const NestedPageorComponent = () => {
const {data, error, loading } = useGet();
if(error) return 'error';
if(loading) return 'loading';
return <></>;
}
Hopefully this is helpful!
Note I wrote most of this on Stack in the editor so I was unable to test the code but it should provide a solid example

Handling auth with React Hooks

I've been chasing my tail for hours now trying to figure out how to handle auth on my component using firebase and react hooks.
I've created a custom useAuth hook that is intended to handle all the auth behaviors. My thought was to put a useEffect on the root of my component tree that would trigger if the firebase.auth.onAuthStateChanged() ever changed (ie, user is now logged out / logged in.) But, at this point after making a million unsuccessful changes I really don't know what I'm doing anymore.
Here is the code that I have...
RootPage component
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [authStatus]);
...
}
my thought was ok, lets trigger authStatus on mount, but that ends up with me lying about my dependencies. So, in an effort to not lie about my deps, I added authStatus to the deps. Logging out and then logging in results in this:
useAuth hook
const useAuth = () => {
const { fetchUser, resetUser, userData } = useUser();
const { currentUser } = firebaseAuth;
const registerUser = async (username, email, password) => {
try {
const credentials = await firebaseAuth.createUserWithEmailAndPassword(
email,
password
);
const { uid } = credentials.user;
await firebaseFirestore
.collection('users')
.doc(credentials.user.uid)
.set({
username,
points: 0,
words: 0,
followers: 0,
following: 0,
created: firebase.firestore.FieldValue.serverTimestamp(),
});
fetchUser(uid);
console.log('user registered', credentials);
} catch (error) {
console.error(error);
}
};
const loginUser = async (email, password) => {
try {
// login to firebase
await firebaseAuth.signInWithEmailAndPassword(email, password);
// take the current users id
const { uid } = firebaseAuth.currentUser;
// update the user in redux
fetchUser(uid);
} catch (error) {
console.error(error);
}
};
const logoutUser = async () => {
try {
// logout from firebase
await firebaseAuth.signOut();
// reset user state in redux
resetUser();
return;
} catch (error) {
console.error(error);
}
};
const authStatus = () => {
firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
return;
}
console.log('User logged out.');
});
};
return { currentUser, registerUser, loginUser, logoutUser, authStatus };
};
export default useAuth;
I'm relatively certain that react hooks are only meant for reusable pieces of logic, so if the purpose of your hook is to contact firebase in every single component you're using it, along with rerendering and refreshing state every time that component is updated, then it's fine, but you can't use hooks for storing global auth state, which is how auth should be stored.
You're looking for react context instead.
import React, {createContext, useContext, useState, useEffect, ReactNode} from 'react'
const getJwt = () => localStorage.getItem('jwt') || ''
const setJwt = (jwt: string) => localStorage.setItem('jwt', jwt)
const getUser = () => JSON.parse(localStorage.getItem('user') || 'null')
const setUser = (user: object) => localStorage.setItem('user', JSON.stringify(user))
const logout = () => localStorage.clear()
const AuthContext = createContext({
jwt: '',
setJwt: setJwt,
user: {},
setUser: setUser,
loading: false,
setLoading: (loading: boolean) => {},
authenticate: (jwt: string, user: object) => {},
logout: () => {},
})
export const useAuth = () => useContext(AuthContext)
const Auth = ({children}: {children: ReactNode}) => {
const auth = useAuth()
const [jwt, updateJwt] = useState(auth.jwt)
const [user, updateUser] = useState(auth.user)
const [loading, setLoading] = useState(false)
useEffect(() => {
updateJwt(getJwt())
updateUser(getUser())
}, [])
const value = {
jwt: jwt,
setJwt: (jwt: string) => {
setJwt(jwt)
updateJwt(jwt)
},
user: user,
setUser: (user: object) => {
setUser(user)
updateUser(user)
},
loading: loading,
setLoading: setLoading,
authenticate: (jwt: string, user: object) => {
setJwt(jwt)
updateJwt(jwt)
setUser(user)
updateUser(user)
},
logout: () => {
localStorage.removeItem('jwt')
localStorage.removeItem('user')
updateJwt('')
updateUser({})
setLoading(false)
},
}
return <AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
}
export default Auth
...
// app.tsx
import Auth from './auth'
...
<Auth>
<Router/>
</Auth>
// or something like that
...
import {useAuth} from './auth'
// in any component to pull auth from global context state
You can change that according to whatever you need.
I know the issue why its happening but don't know the solution...But i am not fully sure...Look how react works is if any parents re render it also cause re render the children..ok?Its mean if any reason your apps is re rendering and the useAuth keep firing...so for this there to much console log.But i am not sure that it will work or not..give me your repo i will try on my local computer
const RootPage = ({ Component, pageProps }): JSX.Element => {
const { logoutUser, authStatus,currentUser } = useAuth();
const router = useRouter();
useEffect(() => {
authStatus();
}, [currentUser]);
//only fire when currentUser change
...
}
Update your useEffect hook like so:
useEffect(() => {
const unsub = firebaseAuth.onAuthStateChanged((user) => {
if (user) {
console.log('User logged in.');
// On page refresh, if user persists (but redux state is lost), update user in redux
if (userData === initialUserState) {
console.log('triggered');
// update user in redux store with data from user collection
fetchUser(user.uid);
}
} else {
console.log('User logged out.');
}
});
return ()=> unsub;
},[])

HOC useEffect firing before Authentication Context ReactJs

I have an Authentication Context that uses useEffect for getting data from sessionStorage and set a global user variable to pass down via context api.
On each protected route, I have a useEffect inside my hoc to check if the user is logged, and if it isn't send the user back to login page.
However, the useEffect inside the hoc is firing before the Authentication Context and therefore, doesn't see the authentication data and sends the customer to login page every time
const router = useRouter()
const [ user, setUser ] = useState(null)
const [ loading, setLoading ] = useState(false)
useEffect(() => {
async function loadUserFromSessionStorage() {
const token = sessionStorage.getItem('accessToken')
if (token) {
const { data: { customer: { name } } } = await axios.get(`http://localhost:3002/customer/token/${token}`)
if (name) setUser(name)
}
setLoading(false)
}
loadUserFromSessionStorage()
})
useEffect(() => {
if(!!user) router.push('/')
}, [ user ])
return (
<AuthContext.Provider
value={{ isAuthenticated: !!user, loading, user}}
>
{children}
</AuthContext.Provider>
)
}
And this is my HOC:
return () => {
const { user, isAuthenticated, loading } = useContext(AuthContext);
const router = useRouter();
useEffect(() => {
if (!isAuthenticated && !loading){
router.push("/login")
}
}, [ loading, isAuthenticated ]);
return (
isAuthenticated && <Component {...arguments} />
)
};
}
Does anyone know how to solve this?
As you may or may not know, the useEffect hook in your HOC will fire before the hook that loads your user from session state completes.
Where you went wrong is in setting your loading state to false by default:
const [ loading, setLoading ] = useState(false)
When you do this in the HOC effect hook
if (!isAuthenticated && !loading)...
That expression will be true the first time through and you get redirected to the login page. Just do useState(true) instead.
You're not passing the loading variable into the context, so when you deconstruct the context value in your HOC, it looks like:
{
user: undefined,
isAuthenticated: false,
loading: undefined
}
which, based on your logic will redirect to login.
Try adding the other two variables inside your AuthContext.Provider and see if that helps you out.

Resources