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

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.

Related

Wait for context value to load before making an API call

I have a user context that loads the user data. I am using that data to send API requests in useEffect. The time lag in the loading of the data is causing an undefined variable in my API request. How do I make the useEffect wait for the context variable to load before sending the request?
This is the UserContext.js:
import { createContext, useState } from "react";
const UserContext = createContext({});
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
This is the custom hook:
import { useContext } from "react";
import UserContext from "../context/UserProvider";
const useUser = () => {
return useContext(UserContext);
};
export default useUser;
And this is the API call in the profile page:
const { user } = useUser();
useEffect(() => {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}, []);
How can I ensure user data is loaded before I make a request throughout my app?
In react, Context APi static data will be Passed to the Children at initial Load. But if you are using asynchronous data in context api, you have to use useEffect and add context value as dependency..
export const UserProvider = ({ children }) => {
const [user, setUser] = useState({});
// Updating Context value Asynchronously..
setTimeout(() => {
setUser("data");
}, [3000]);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
};
const { user } = useUser();
useEffect(() => {
// Call api only if user contains data.
if(user != {}) {
Axios.get(
`API_URL/${user?.subscription_id}`
).then((res) => {
console.log(res)
});
}
}, [user]);

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>
)
}

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;
},[])

Redirect react router with redux-saga

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);

Resources