Redirect react router with redux-saga - reactjs

I have redux-saga which should redirect user to main page after successfully logging. I do not use react-router-redux or smth like that so my router is not connected to state.
Here is my Login component
const AdminLogin: React.FC<RouteComponentProps> = ({history}) => {
const [form, setForm] = useState({username: '', password: ''});
const user = useSelector(getAdminUser);
const dispatch = useDispatch();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(makeLoginAsync(form));
};
useEffect(() => {
if (user && user.token) {
history.push('/admin/main'); // here it is
}
}, [user, history]);
return (
<form onSubmit={handleSubmit} className="admin-login">
// FORM HTML CODE
</form>
);
};
export default withRouter(AdminLogin);
Notice I'm dispatching form to Action, then Saga is listening my Action and does all logic and effects. I thought it would be much better to do redirection in Saga code. But I couldn't?
After successful login, Reducer changes the state sets user auth token. If I have token, I take it as user is logged in and redirect him to another page.
As you see, I implement this logic directly in the component (using Selector). But I find it very ugly.
Actually, I can access history object only in component (if component is wrapped with withRouter).
If I used react-router-redux, I could do something like
import { push } from 'react-router-redux'
and use push function to redirect from Saga. I'm new to React and I heard that it's not obligatory to connect router to redux. Also I don't want to make my application more complex and have another dependency to implement such basic feature as redirection.
So now I have to make it even more complex and implicitly. Maybe I have missed something?
Any help is welcome.

pass history with dispatch event and then use it to push route inside redux
const AdminLogin: React.FC<RouteComponentProps> = ({history}) => {
const [form, setForm] = useState({username: '', password: ''});
const user = useSelector(getAdminUser);
const dispatch = useDispatch();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
dispatch(makeLoginAsync({form,history}));
};
useEffect(() => {
if (user && user.token) {
history.push('/admin/main'); // here it is
}
}, [user, history]);
return (
<form onSubmit={handleSubmit} className="admin-login">
// FORM HTML CODE
</form>
);
};
export default withRouter(AdminLogin);

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 stop multiple calls of the same query using React-query?

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.

React and Firebase: onAuthStateChanged problem, state update on unmounted component

This is my Context file, and I have my App.js wrapped with the AuthProvider:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, user => setUser(user));
return () => { unsubscribe(); }
}, []);
return (
<AuthContext.Provider value={{ user }}>{children}</AuthContext.Provider>
);
};
This is one of those consumer component.
function PurchasesNSales() {
const { user } = useContext(AuthContext);
const history = useNavigate();
useEffect(() => {
if (!user)
history("/", { replace: true });
}, [user, history]);
return(
<p>Welcome {user?.displayName}</p>
);
}
export default PurchasesNSales;
If I remove the useEffect on the second file, everything works fine, but if I do that I am not verifying authentication of the user.
I am most likely doing something wrong.
From what I understand I am trying to setState on a component that is already unmounted, which I don't understand where that is happening.
I am on my Dashboard page, I press the button which should take me to the "PurchasesNSales" page and that is when the error occurs.
Codesandbox Hopefully this sample app helps.

aws-amplify-react and nextjs breaks my user context

I cant figure out why but when I use cognito with my own custom user context everything works just fine but as soon as I use withAuthenticator higher order component it breaks my user context and I cant for the life of me figure out why, or even how to fix it. Ill post my user context file below for reference and tell you where it breaks.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
//TODO must redo cognito from scratch and will probably be able to keep this user context untouched
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser().then(x=>setUser(x)).catch((err)=>setUser(null))
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false)
Auth.signIn(username, password)
.then( x => {
setUser(x)
console.log('Welcome: ' + x.challengeParam.userAttributes.email)
setUserEmail(x.challengeParam.userAttributes.email)
setSignInError(false)
})
.catch((err)=>{
console.log(err.code)
if(err.code === 'UserNotFoundException' || 'NotAuthorizedException'){
err.message = 'Invalid username or password'
setSignInError(true)
console.log(err.message)
}
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
return x
})
}
const signup = (username, email, password) => {
Auth.signUp({ username, password, attributes: { email } })
.then( x => {
setUser(x)
return x
})
.catch((err)=>{
if(err.code){
err.message = 'Your Username or Password was incorrect'
}
throw err
})
}
const vals = useMemo( () => ({user, login, logout, signup, handleSignInError, userEmail, signInError}), [user, userEmail, signInError])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}
Under the login function it now returns user not found after I wrap a component and npm i aws-amplify-react. The funny thing is when I uninstall it I still get the same error and cant go back without fully removing amplify and going through a complete amplify init again. Even more confusing, My app is hosted on vercel and that breaks after I attempt to do this on my local machine. If im not missing something there and my app does break in the cloud even though I dont push my modified code then im guessing cognito is getting something in the cloud when I attempt this on my local machine and then screwing up my untouched copy on vercel????? Since then Ive also tried using next-auth which makes me think I should just stick to front end work or find a better solution? any help would be appreciated. Ill revert to my old setup and rebuild my cognito and amplify from scratch just to get it going again.
You need to call Cognito configure prior to calling your auth provider. Place it before you define your auth provider or context.
Auth.configure({...your_config})
const UserContext = () => {};
I also use a auth hook with my context that removes the need for a HOC.
import { useContext } from 'react';
export const useAuth = () => useContext(UserContext);
// use it in components and pages
const user = useAuth();
Ensure that your configuration is using all of the proper types. If you don't, it sometimes fails silently. For example ENV files are always passed as strings so some options must be cast to the proper type like cookie expires
{
authenticationFlowType: 'USER_SRP_AUTH',
cookieStorage: {
...other settings
expires: Number(process.env.NEXT_PUBLIC_COGNITO_COOKIE_EXPIRES),
}
};
You will also need to call Auth.configure on every page that you need access to Congito auth inside of getStaticPaths, getStaticProps, and getServerSideProps. This is because they are independently called from your app during build or on a server.
Auth.configure({...your_config})
const getStaticProps = () => {};
const getStaticPaths = () => {};
const getServerSideProps = () => {};
If you can use it, their hosted UI is pretty good.
Lastly, AWS has a few libraries for Amplify and I use #aws-amplify/auth - I don't know if this makes a difference.
I added the config file to my _app.js and set ssr: true for ssr authentication
import Amplify from 'aws-amplify'
import config from '../src/aws-exports'
Amplify.configure({...config, ssr: true})
Here is my working user context. I removed the signup function and will add it later once i work on it and test it.
import { Auth } from 'aws-amplify'
import {createContext, useState, useEffect, useMemo} from 'react'
export const UserContext = createContext(null)
export const UserProvider = ({children}) => {
const [ user, setUser ] = useState(null)
const [ userEmail, setUserEmail ] = useState(null)
const [ signInError, setSignInError ] = useState(false)
const [sub, setSub] = useState(null)
useEffect(()=>{
// AWS Cognito
Auth.currentAuthenticatedUser()
.then(x=>{
setUser(x.username)
setUserEmail(x.attributes.email)
setSub(x.attributes.sub)
})
.catch((err)=>{
console.log(err)
setUser(null)
})
},[])
const handleSignInError = () => {
console.log(signInError)
}
const login = (username, password) => {
signInError && setSignInError(false);
Auth.signIn(username, password)
.then((x) => {
setUser(x.username)
setSignInError(false)
console.log(x)
})
.catch((err)=>{
console.log(err)
setSignInError(true)
})
}
const logout = () => {
Auth.signOut().then((x)=>{
setUser(null)
setUserEmail(null)
setSub(null)
})
}
}
const vals = useMemo( () => ({user, sub, login, logout, handleSignInError, userEmail, signInError}), [user, userEmail, signInError, sub])
return(
<UserContext.Provider value={vals}>
{children}
</UserContext.Provider>
)
}

Resources