Firebase authentication & Public / Private Routing - React - reactjs

In my react application, I am trying to implement Public and Private routes with react-router-dom.
I am currently getting the authentication state from firebase.auth().onAuthStateChanged() function.
The problem is that since the firebase.auth().onAuthStateChanged() function is asynchronous, JSX block is rendered with authState of false first, and then firebase.auth().onAuthStateChanged() sets authState to true after the JSX is returned.
So, the authentication is true, but my react app stays at the sign-in page.
PublicRoute.js
const PublicRoute = ({ component: Component, restricted, ...rest }) => {
let authState = false;
getAuthState()
.then(user => {
if (user.uid) {
authState = true;
} else {
authState = false;
}
})
return (
<Route { ...rest } render={ props => (
authState && restricted
? <Redirect to="/" />
: <Component { ...props } />
) } />
)
};
export default PublicRoute;
getAuthState.js
export const getAuthState = () => {
return new Promise((resolve, reject) => {
const waitAuthStateChange = () => {
let currentUser = firebase.auth().currentUser;
if (currentUser === null) {
firebase.auth().onAuthStateChanged(user => currentUser = user);
setTimeout(waitAuthStateChange, 100);
} else {
resolve(currentUser);
}
}
waitAuthStateChange();
})
};
I don't know how to make re-render or re-return the JSX after the authentication is fetched by firebase.auth().onAuthStateChanged() listener.
Thank you in advance!!

You will need a React state.
To use it, you can use it with a hook called useState();
Example,
const [authState,setAuthState] = useState(false);
getAuthState()
.then(user => {
if (user.uid) {
setAuthState(true);
}
})
Your current authState is a normal javascript object, so React doesn't regard it as a change and therefore, won't re-render.
Change it to React state and then when you do a setAuthState() , React use it as a pivot for change and will render again.
Side Note: Calling getAuthState() by itself is not a good practice in React.
In React way, it should be put inside the useEffect() hook.
Reference for useState: https://reactjs.org/docs/hooks-state.html
Reference for useEffect: https://reactjs.org/docs/hooks-effect.html

Related

NextJS + Supabase - Blank Page Issue

I am attempting to render either an Application or Login page depending on whether getUser() returns a user object.
However, in both development and production, a blank page is rendered.
This is the code
export default function index() {
supabase.auth.getUser().then((response) => {
const userData = response.data.user;
console.log(userData);
return userData != undefined || userData != null ? (
<>
<Shell />
<AppView />
</>
) : (
<NoSessionWarn />
);
});
}
I use NextJS's router.push('/application') to route the user to this page, in case that might have something to do with it.
Any idea why this could be showing a blank page? I've tried taking the return block out of the .then() block and still nothing.
Few things:
In React functional components, side effects must be handled inside
a useEffect hook
React components names should be capitalized (Index instead of index in your case).
Most of the time it's a better idea to use strict equality operator since it also checks for the type of the operands.
As a suggestion, you could abstract the logic of the auth checking process into a custom hook. This not only increases the readability of the component, but also makes this logic reusable and you now would have separation of concerns. Your component doesn't know and doesn't care about how the user data is being retrieved, it just uses it.
Putting it all together:
useAuth custom hook:
export const useAuth = () => {
const [user, setUser] = useState(null)
const [isAuthorizing, setIsAuthorizing] = useState(true)
useEffect(() => {
supabase.auth
.getUser()
.then((response) => {
setUser(response.data.user)
})
.catch((err) => {
console.error(err)
})
.finally(() => {
setIsAuthorizing(false)
})
}, [])
return { user, isAuthorizing }
}
Component:
export default function Index() {
const { user, isAuthorizing } = useAuth()
if (isAuthorizing) return <p>Loading</p>
// Being very explicit here about the possible falsy values.
if (user === null || user === undefined) return <NoSessionWarn />
return (
<>
<Shell />
<AppView />
</>
)
}
You need to use the useState hook to re-render when you receive the data.
You need to use the useEffect hook with an empty dependency array to execute getUser() once on mount.
You'll also probably want a loading mechanism while the request is made.
export default function index() {
const [userData, setUserData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
supabase.auth.getUser().then((response) => {
setUserData(response.data.user);
setLoading(false);
});
}, []);
if (loading) return <p>Loading...</p>
if (!userData) return <NoSessionWarn />;
return (
<>
<Shell />
<AppView />
</>
);
}
Example: https://stackblitz.com/edit/react-ts-neq5rh?file=App.tsx

Route to page inside useEffect

I'm trying to produce a minimal example of routing to login if no session is found. Here is my code from _app.js inside pages folder :
function MyApp({ Component, pageProps }) {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const session = document.cookie.includes("session_active=true")
if (session) {
fetch("/api/user")
.then(u => u.json().then(setUser))
} else {
const redirectURI = router.pathname
const url = {pathname: "/login", query: {"redirect_uri": redirectURI}}
router.push(url)
}
}, [])
if (!user) return Loading()
return (<div>User {user.name} {user.surname}</div>)
}
My login is inside pages/login.js with this content :
const Login = () => (<div>Login page</div>)
export default Login
However it's stuck on the loading page even though I don't have the session. Am I misusing the router ?
The URL is changed properly to /login?redirect_uri=%2Ffoo but the content is not the one from my Login
Below is a stackblitz reproduction: https://stackblitz.com/edit/github-supacx-rpl5rm
I see the problem, You are preventing the app to load.
You are not changing user's state in case there is no session_active cookie.
You are trying to render the only loading component instead of the next App.
if (!user) return Loading()
Solution:
Let the app render
render the loading component inside the return statement of the app component
import React, { useState, useEffect } from 'react'
import { useRouter } from 'next/router'
export default function App({ Component, pageProps }) {
const [user, setUser] = useState(null)
const router = useRouter()
useEffect(() => {
const session = document.cookie.includes('session_active=true')
if (session) {
fetch('/api/user').then((u) => u.json().then(setUser))
} else {
setUser(true) // set to true.
const redirectURI = router.pathname
const url = { pathname: '/login', query: { redirect_uri: redirectURI } }
router.push(url)
}
}, [])
return (
<>
{!user && <div>loading</div>}
<Component {...pageProps} />
</>
)
}
I am not sure which approach you will use to pass user info to all components. My suggestion would be to create a context for authentication and wrap the app with it. Then handle the user session and redirection in the context.

How to run use effect in App.js while using react router dom

I want to run getUser function every time the user goes to some other link.
The following is my getUser function
const getUser = async () => {
if (localStorage.getItem('access') === null || localStorage.getItem('refresh') === null || localStorage.getItem('user') === null) {
setUser({ email: null });
setIsLoggedIn(false);
return;
}
const responseForAccessToken = await verifyTokenAPI(localStorage.getItem('access'));
console.log(responseForAccessToken);
if (responseForAccessToken.status >= 400) {
const newAccessTokenResponse = await getAccessTokenAPI(localStorage.getItem('refresh'));
console.log(newAccessTokenResponse);
if (newAccessTokenResponse.status >= 400) {
localStorage.removeItem('access');
localStorage.removeItem('refresh');
localStorage.removeItem('user');
setUser({ email: null });
setIsLoggedIn(false);
return;
}
localStorage.removeItem('access');
localStorage.setItem('access', newAccessTokenResponse.access);
}
I want to verify token every time the user changes routes. Therefore, I used getUser function in useEffect in my App.js
const history = useHistory();
const { getUser } = useAuth();
useEffect(() => {
history.listen((location) => {
console.log(`You changed the page to: ${location.pathname}`);
});
getUser();
}, [history]);
Every time I change routes the useEffect runs and console logs the message but does not run getUser function.
I am using Link from react-router-dom
<h1>Welcome {user.email}</h1>
<Link to="/protected-route-2">Protected Route 2</Link>
<button
onClick={() => logout({ callBack: () => history.push("/login") })}
>
Logout
</button>
Additionally, I also have a PrivateRoute component
const Privateroute = ({ component: Component, ...rest }) => {
const { isLoggedIn, getUser } = useAuth()
console.log(isLoggedIn);
const location = useLocation()
if (isLoggedIn) return <Route {...rest} render={props => <Component {...props} />} />;
return <Redirect to={{ pathname: '/login', state: { from: location.pathname } }} />
}
I am not sure if I am doing things right. Can someone suggest another approach to this problem? Any suggestion will be appreciated. Thanks in advance.
You should use the useLocation hook (as shown in the documentation) instead of the useHistory, which would give you the current location and use that as the dependency for the useEffect:
const location = useLocation();
const { getUser } = useAuth();
useEffect(() => {
console.log(`You changed the page to: ${location.pathname}`);
getUser();
}, [location]);
In your code, the history object does not change and the effect is only fired once, the reason you keep getting the console logs when the location changes is that you added a listener to the history.

React Native data in context is undefined on the first render

I use AppContext, when I fetch data from server I want it to save in context but on the first render it doesn't save. If I make something to rerender state data appears in context.
Here is my code:
useEffect(() => {
fetch('https://beautiful-places.ru/api/places')
.then((response) => response.json())
.then((json) => myContext.updatePlaces(json))
.then(() => console.log('jsonData', myContext.getPlaces()))
.catch((error) => console.error(error));
}, []);
My getPlaces and updatePlaces methods:
const [allPlaces, setAllPlaces] = useState();
const getPlaces = () => {
return allPlaces;
};
const updatePlaces = (json) => {
setAllPlaces(json);
};
const placesSettings = {
getPlaces,
updatePlaces,
};
Here is how I use AppContext:
<AppContext.Provider value={placesSettings}>
<ThemeProvider>
<LoadAssets {...{ assets }}>
<SafeAreaProvider>
<AppStack.Navigator headerMode="none">
<AppStack.Screen
name="Authentication"
component={AuthenticationNavigator}
/>
<AppStack.Screen name="Home" component={HomeNavigator} />
</AppStack.Navigator>
</SafeAreaProvider>
</LoadAssets>
</ThemeProvider>
</AppContext.Provider>;
Could you explain please why my console.log('jsonData', ...) returns undefined?
I don't understand because on previous .then I saved it.
Edit to note that the code below is not copy-paste ready. It is an example of how to attack the problem – you will need to implement it properly in your project.
The 'problem' is that hooks are asynchronous – in this specific case, your useEffect further uses an asynchronous fetch too.
This means that the data that is returned by the fetch will only be available after the component has rendered, and because you're not updating state/context using a hook, the context won't update.
The way to do this requires a few changes.
In your context implementation, you should have a setter method that sets a state variable, and your getter should be that state variable.
placesContext.js
import React, { createContext, useState } from "react";
export const placesContext = createContext({
setPlaces: () => {},
places: [],
});
const { Provider } = placesContext;
export const PlacesProvider = ({ children }) => {
const [currentPlaces, setCurrentPlaces] = useState(unit);
const setPlaces = (places) => {
setCurrentPlaces(places);
};
return (
<Provider value={{ places: currentPlaces, setPlaces }}>{children}</Provider>
);
};
Wrap your App with the created Provider
App.js
import { PlacesProvider } from "../path/to/placesContext.js";
const App = () => {
// ...
return (
<PlacesProvider>
// Other providers, and your app Navigator
</PlacesProvider>
);
}
Then, you should use those variables directly from context.
MyComponent.js
import { placesContext } from "../path/to/placesContext.js";
export const MyComponent = () => {
const { currentPlaces, setPlaces } = useContext(placesContext);
const [hasLoaded, setHasLoaded] = useState(false);
useEffect(() => {
async function fetchPlacesData() {
const placesData = await fetch('https://beautiful-places.ru/api/places');
if (placesData) {
setPlaces(placesData);
} else {
// error
}
setHasLoaded(true);
}
!hasLoaded && fetchPlacesData();
}, [hasLoaded]);
return (
<div>{JSON.stringify(currentPlaces)}</div>
)
};

How to correctly redirect to the login page with React

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

Resources