I have react native app and i am implementing auth flow using Context API
the nature of the app is when user open it won't request to login or signup and user can explore the app screens however when user add items to cart and about to checkout then will be requested to sign-in/sign-up to continue.
let's assume these are the screens
Home-> shop -> cart
so when user in the cart will be asked to login, after login user value in context provider will be updated and they continue from same screen the user logged in (which in this case is cart screen)
However, when provider value updated all screens re-render again and redirect to initial route which home screen.
how can i handle this scenario which not re-render all screen again.
In my app i am passing the navigation stuck as a children to context provider as follow:
<AuthProvider>
<Routes />
</AuthProvider>
and here is the code of context provider:
authProvider.js:
export const AuthContext = createContext();
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const value = useMemo(
() => ({ user, setUser }),
[user]);
return (
<AuthContext.Provider
value={{
value,
login: async (email, password) => {
//here i am validating the login details and update user state
// if login successful
setUser(/*some data related to user */)
//when i set user data here it re-render all screens again
// if login failed
setUser(null)
}
}}>
{children}
</AuthContext.Provider >
and the following the Routes.js which i am passing it as children to the above code:
const Routes = () => {
const { value } = useContext(AuthContext);
// validate token with backend
useEffect(() => {
setTimeout(async () => {
const Token = await AsyncStorage.getItem('Token');
if (Token === null) {
value.setUser(null)
} else {
await axios.get(URL, {
headers: { 'x-auth-token': Token },
}).then(async (res) => {
const {data} = res;
value.setUser(data)
}).catch(async (err) => {
if (err.response) {
await AsyncStorage.removeItem('Token');
value.setUser(null)
}
});
}
}, 1000)
}, []);
return (
<>
<NavigationContainer>
//this is the stuck where i have the three screen mentioned above (home, shop, cart)
<AppStuckScreen />
</NavigationContainer>
</>
)
}
export default Routes;
In cart screen i am checking the user values stored in context provider if null then open the login form.
I hope someone can help me on how to handle this situation where not to re-render all component again when updating user value in Context provider.
Why don't you keep the current screen in the Context? So after a successful login, you just navigate to that screen.
Related
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.
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 am using expo and AWS Cognito for my app. I am trying to divert users who are authenticated to the home screen while those who are not to the sign-in screen but cannot get the expected behavior to work. The main issue seems to be when users sign out the app does not refresh, so the AWS token is being pulled from local storage so that the user still appears to be logged in. Only when I do a hard refresh does it show they are now logged out.
Here is my App.js code to log in the user:
const App = () => {
const [userID, setUserID] = useState(null);
useEffect(() => {
const fetchUser = async () => {
const userInfo = await Auth.currentAuthenticatedUser(
{ bypassCache: true }
);
console.log(userInfo.attributes.sub);
if (!userInfo) {
return;
}
if (userInfo) {
const userData = await API.graphql(
graphqlOperation(
getUser,
{ id: userInfo.attributes.sub,
}
)
)
if (userData.data.getUser) {
console.log(userData.data.getUser);
setUserID(userData.data.getUser)
return;
} else {
setUserID(null);
}
}
}
fetchUser();
}, [])
return (
<AppContext.Provider value={{
userID,
setUserID: ({}) => setUserID({}),
}}>
<AppNavigation/>
</AppContext.Provider>
);
}}
And here is my code for the App Navigation where I have my conditional statement:
const AppNavigation = () => {
const { userID } = useContext(AppContext);
console.log(userID) //this gives the correct value, null when not logged in and a user object when the user is logged in.
return (
<NavigationContainer>
<Drawer.Navigator
drawerContent={props => <DrawerContent { ...props} />}
drawerPosition='left'
initialRouteName={userID === null ? 'SignIn' : 'HomeDrawer'}
>
<Drawer.Screen
name='HomeDrawer'
component={MainNavStack}
/>
<Drawer.Screen
name='SignIn'
component={SignIn}
/>
</Drawer.Navigator>
</NavigationContainer>
I have tried every combination of useState and useEffect I can think of:
using getAuthenticatedUser on the AppNavigation screen and then setting a state if successful.
passing props from App.js directly
not using AppContext at all
putting it all in useEffect
getting the authenticated user directly from a function in the conditional statement
The problem seems to be that initialRouteName is determining the route before I can set any kind of state in the app. How can I get the expected behavior without having to hard refresh the app?
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.
I have the following (redux) state:
{
authentication: user
}
When logged out, user is set to null.
I have the following components:
const Dashboard = ({ authentication }) => {
if (!authentication.user) {
return <Redirect to={"/login"} />
}
return (
<SomeInnerComponent />
);
}
const SomeInnerComponent = ({ authentication }) => {
const name = authentication.user.name;
return (
<h1>Hello, {name}</h1>
)
}
authentication is mapped using connect and mapStateToProps. I would think that when I am logged out that I would be redirected, but I get an error instead: authentication.user is null.
Why does the if-statement in Dashboard not redirect me? I also tried wrapping it in a useEffect with authentication as a dependency.
In our app, we redirect unauthenticated users by history.replace history docs
or you read docs again, maybe you can find mistake in your code reacttraining
I fixed it by writing a custom hook:
export function useAuthentication() {
const history = useHistory();
const user = useSelector(state => state.authentication.user);
const dispatch = useDispatch();
useEffect(() => {
if (!user) {
history.push(LOGIN);
});
return { user };
}
Which can then be called in my React components as follows:
const Dashboard = () => {
const { user } = useAuthentication();
return (
// My code
);
}