I have all my api functions in a special folder. When I use them, I sometimes have to update the context, which stores the user's informations.
However, the app crashes when I try to access its value directly from within these functions. So I always have to import the context in the component, and then pass the function setUser as a parameter to the api function. It is a bit messy. How to properly access and update the context's state in my api service?
Here is a classic function:
export async function useUpdatePassword(
email: string,
password: string,
token
) {
const { setUser } = useUser();
try {
const res = await ax.post(process.env.SERVER_URL + "/update-password", {
email,
password,
token,
});
localStorage.setItem("myapp", res.data.token);
setUser(jwt_decode(res.data.token));
return "update password done!!"
} catch (err) {
return "error"
}
}
And my context:
const UserContext = createContext(null);
export default function UserProvider({ children }) {
const [user, setUser] = useState<User>();
useEffect(() => {
fetchUser(setUser); // fetch user infos in my db
}, []);
return (
<UserContext.Provider value={{ user, setUser }}>
{children}
</UserContext.Provider>
);
}
export function useUser() {
return useContext(UserContext);
}
If there is no way to do it with Context, is it possible to use Redux, Jotai or any other library to do so?
Related
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.
I have created Firebase Authentication with ReactJS.
Everything works fine until The page refreshes. I'm using onAuthStateChanged listener, however, after I'm refreshing the page, it navigates me back to the Login form.
UserAuthContext.js
const userAuthContext = createContext();
export function UserAuthContextProvider({ children }) {
const [user, setUser] = useState("");
function signUp(email, password, username) {
return createUserWithEmailAndPassword(auth, email, password);
};
function logIn(email, password) {
return signInWithEmailAndPassword(auth, email, password);
};
function logout() {
return signOut(auth);
}
useEffect(() => {
const subscribe = onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser);
});
return subscribe;
}, []);
return (
<userAuthContext.Provider value={{ user, signUp, logIn, logout }}>
{children}
</userAuthContext.Provider>
);
}
export function useUserAuth() {
return useContext(userAuthContext);
}
By the way, I'm using siteground hosting, so is there any way to store it?
You are saving the the user to a react state hook. React state does not persist data after page reload. A common way people persist data is by saving it into local storage or a cookie. However, these methods are prone to attacks like XSS and CSRF.
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]);
The situation is as follows. In my application, I use a router, and pass information to it whether the user is identified or not, similar to the role of the administrator. The data is stored in the auth context, put there auth hook, and used in App.js. The entire code is below. The problem is that when I reload the page, I get redirected from any tab to the home page. This happens because of a momentary change in App.js when constructing the App component, the variables in the useRoutes(isAuthenticated, admin) function change to false and true(true - after identification) when the page is reloaded. I'm relatively new to React, and don't really understand how to solve this issue. All I want to achieve is to make sure that the variables don't change their values in the App by simply refreshing the page.
App.js
function App() {
const { token, login, logout, admin } = useAuth()
const isAuthenticated = !!token
const routes = useRoutes(isAuthenticated, admin)
return (
<AuthContext.Provider value={{
token, login, logout, isAuthenticated, admin
}}>
<Router>
<div className="app-container">
{routes}
</div>
</Router>
</AuthContext.Provider>
);
}
AuthContext.js
function noop() { }
export const AuthContext = createContext({
token: null,
login: noop,
logout: noop,
isAuthenticated: false,
admin: false,
})
Auth.hook.js
const storageName = 'userData'
export const useAuth = () => {
const [token, setToken] = useState(null)
const [admin, setAdmin] = useState(false)
const login = useCallback((jwtToken, isAdmin) => {
setToken(jwtToken)
setAdmin(isAdmin)
localStorage.setItem(storageName, JSON.stringify({
token: jwtToken,
isAdmin: isAdmin,
}))
}, [])
const logout = useCallback(() => {
setToken(null)
setAdmin(false)
localStorage.removeItem(storageName)
}, [])
useEffect(() => {
const data = JSON.parse(localStorage.getItem(storageName))
if (data && data.token) {
login(data.token, data.isAdmin) // <-- There some problem
}
}, [login])
return { login, logout, token, admin }
}
I solved this problem with a simple solution :)
All you need to do is just add 1 more variable ready in the context with a value of false . Then set it in auth.hook.js to true after login. And export it to App.js and use it like if(ready){return "page"} else return <>Loading</>.
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>
)
}