Why is this React Component rendering first? - reactjs

I could use your input on a quick question about Component loads.
The Goal
Return the <Login /> Component if the user isn't logged in, and the App if they are.
Expected Behavior
When a user is logged in, they see the App.
Observed Behavior
The <Login /> Component flickers (renders) for a moment, then the user sees the App.
My goal is to eliminate this flicker!
Code Samples
Index.js
export default function Index() {
let [isLoading, setIsLoading] = useState(true)
const router = useRouter()
// User object comes in from an Auth Context Provider
const { user } = useContext(AuthContext)
const { email } = user
useEffect(() => {
if (user) {
setIsLoading(false)
}
}, [])
// Returns the App if logged in, login screen if not
const getLoggedIn = () => {
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
}
return (
<Box className="App">
{ isLoading
? <div className={classes.root}>
<LinearProgress />
</div>
: getLoggedIn()
}
</Box>
)
}
Auth Context
Note: I'm using Firebase for auth.
// Listens to auth state changes when App mounts
useEffect(() => {
// Calls setUser state update method on callback
const unsubscribe = onAuthStateChange(setUser)
return () => {
unsubscribe()
}
}, [])
// Brings data from auth to Auth Context user state via callback
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
Stack
"next": "^8.1.0",
"react": "^16.8.6",
"react-dom": "^16.8.6"
Thanks so much for taking a look.

I had this exact problem and resolved it by storing the user in local storage
then on app start up do this:
const [user, setUser] = useState(JSON.parse(localStorage.getItem('authUser')))
and it'll use the details from localstorage and you wont see a flicker
(it's because onauthstate takes longer to kick in)

So I figured out a sort of 'hacky' way around this. One needs to set the value of the boolean on which the initial load of the App depends...
const getLoggedIn = () => {
// Right here
if (user.loggedIn) {
return (
<>
// App goes here
</>
)
} else {
return <Login />
}
...before making any asynchronous calls in the AuthContext. Like this:
const onAuthStateChange = callback => {
return auth.onAuthStateChanged(async user => {
if (user) {
// sets loggedIn to true to prevent flickering to login screen on load
callback({ loggedIn: true })
const userFirestoreDoc = await firestore.collection('users').doc(user.uid).get()
const buildUser = await callback({
loggedIn: true,
email: user.email,
currentUid: user.uid,
userDoc: userFirestoreDoc.data()
})
} else {
callback({ loggedIn: false })
}
})
}
I hope this helps someone.

Related

Using conditional statement in react navigation based on AWS authenticated user

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?

How to keep authenticated state on refresh?

I'm using firebase authentication for my app. I used useAuth hook from here. Integrate with react-router guide about redirect (Auth).
SignIn,SignOut function is working as expected. But when I try to refresh the page. It redirects to /login again.
My expected: Redirect to / route when authenticated.
I tried to add this code in PrivateRoute.js
if (auth.loading) {
return <div>authenticating...</div>;
}
So I can refresh the page without redirect to /login but it only show authenticating... when click the log out button.
Here is my code: https://codesandbox.io/s/frosty-jennings-j1m1f?file=/src/PrivateRoute.js
What I missed? Thanks!
Issue
Seems you weren't rendering the "authenticating" loading state quite enough.
I think namely you weren't clearing the loading state correctly in the useEffect in useAuth when the initial auth check was resolving.
Solution
Set loading true whenever initiating an auth check or action, and clear when the check or action completes.
useAuth
function useProvideAuth() {
const [loading, setLoading] = useState(true); // <-- initial true for initial mount render
const [user, setUser] = useState(null);
// Wrap any Firebase methods we want to use making sure ...
// ... to save the user to state.
const signin = (email, password) => {
setLoading(true); // <-- loading true when signing in
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.then((response) => {
setUser(response.user);
return response.user;
})
.finally(() => setLoading(false)); // <-- clear
};
const signout = () => {
setLoading(true); // <-- loading true when signing out
return firebase
.auth()
.signOut()
.then(() => {
setUser(false);
})
.finally(() => setLoading(false)); // <-- clear
};
// Subscribe to user on mount
// Because this sets state in the callback it will cause any ...
// ... component that utilizes this hook to re-render with the ...
// ... latest auth object.
useEffect(() => {
const unsubscribe = firebase.auth().onAuthStateChanged((user) => {
if (user) {
setUser(user);
} else {
setUser(false);
}
setLoading(false); // <-- clear
});
// Cleanup subscription on unmount
return () => unsubscribe();
}, []);
// Return the user object and auth methods
return {
loading,
user,
signin,
signout
};
}
Check the loading state in PrivateRoute as you were
function PrivateRoute({ children, ...rest }) {
const auth = useAuth();
if (auth.loading) return "authenticating";
return (
<Route
{...rest}
render={({ location }) =>
auth.user ? (
children
) : (
<Redirect
to={{
pathname: "/login",
state: { from: location }
}}
/>
)
}
/>
);
}
Demo
Try this approach, it works for me :
const mapStateToProps = state => ({
...state
});
function ConnectedApp() {
const [auth, profile] = useAuth()
const [isLoggedIn, setIsLoggedIn] = useState(false)
useEffect(() => {
if (auth && auth.uid) {
setIsLoggedIn(true)
} else {
setIsLoggedIn(false)
}
}, [auth, profile]);
return (<Router>
<Redirect to="/app/home"/>
<div className="App">
<Switch>
<Route path="/home"><Home/></Route>
<Route path="/login"><Login styles={currentStyles}/></Route>
<Route path="/logout"><Logout styles={currentStyles}/></Route>
<Route path="/signup" render={isLoggedIn
? () => <Redirect to="/app/home"/>
: () => <Signup styles={currentStyles}/>}/>
<Route path="/profile" render={isLoggedIn
? () => <Profile styles={currentStyles}/>
: () => <Redirect to="/login"/>}/>
</Switch>
</div>
</Router>);
}
const App = connect(mapStateToProps)(ConnectedApp)
export default App;

Redirect to login is always triggered because there is a delay from useContext()

I am trying to return the user to login if he is not authenticated. Right now it is always triggered when I use this:
useEffect(() => {
if (!user) {
router.push('/login')
}
}, [])
The user comes from a context provider I set up at my _app.js. I fetch the user using this:
const { user, userDetails } = useContext(UserContext);
But because there is a delay somehow in fetching it always returns me to login, even when I am authenticated and there is a user.
UserContext.Provider:
<UserContext.Provider
value={{
user: user,
handleLogout: handleLogout,
userDetails: userDetails,
}}
>
<Component {...pageProps} />
</UserContext.Provider>
If user is a boolean you can set it as null
const [user, setUser] = useState(null);
useEffect(()=> {
//--> fetch user and set it true o false
}, []);
then:
useEffect(() => {
if (user === false) {
router.push('/login')
}
}, [user])
I figured it out, thanks to #lissettdm.
I added in _app.js a new value
const [checkUser, setCheckUser] = useState(null);
<UserContext.Provider
value={{
user: user,
handleLogout: handleLogout,
userDetails: userDetails,
checkUser: checkUser,
}}
>
Then I import it and check using useEffect:
const { user, userDetails, checkUser } = useContext(UserContext);
useEffect(() => {
if (checkUser === false) {
router.push("/account");
}
}, [checkUser]);

Apollo client (react) - Cant update state on unmounted component

im trying to implement social authentication in my project and im getting this error:
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
in FaceookSignIn (created by Socials)
...
Component in question recieves code from facebook which is put into url, for redirecting.
This is the route:
<PublicRoute exact path='/:callback?' component={Auth}/>
defined as:
export const PublicRoute = ({component: Component, ...rest}) => {
const {client, loading, data} = useQuery(GET_USER, {fetchPolicy: 'cache-only'})
let isAuthenticated = !!data.user.accessToken
return (
<Route {...rest} component={(props)=>(
isAuthenticated ? (
<Redirect to='/home' />
) : (
<Component {...props} />
)
)}/>
)
}
I've tried using hook cleanup on my component but error persists. This is what my current implementation looks like:
const FaceookSignIn = () => {
let _isMounted = false
const client = useApolloClient()
const appId = '187856148967924'
const redirectUrl = `${document.location.protocol}//${document.location.host}/facebook-callback`;
const code = (document.location.pathname === '/facebook-callback') ? querystring.parse(document.location.search)['?code'] : null
const [loading, setLoading] = useState(false)
const [callFacebook, data] = useMutation(FACEBOOK_SIGN_IN)
useEffect(()=>{
_isMounted = true
if(!code) return
if(_isMounted) callFacebook({variables: {code: code}})
.then(res=>{
const {error, name, email, accessToken} = res.data.facebookSignIn
if (error) {
alert(`Sign in error: ${error}`);
} else {
client.writeData({
data: {
user: {
name: name,
email: email,
accessToken: accessToken,
__typename: 'User'
}
}
})
setLoading(false)
}
})
.catch(e=>{
console.log(e)
setLoading(false)
})
return ()=> _isMounted = false
},[])
const handleClick = e => {
setLoading(true)
e.preventDefault()
window.location.href = `https://www.facebook.com/v2.9/dialog/oauth?client_id=${appId}&redirect_uri=${encodeURIComponent(redirectUrl)}`;
}
return (
<a className="login-options__link" href='/facebook-login' onClick={handleClick}>
{loading ? <p>loading...</p> : <img className="social-link__icon" src={fb.default} id="facebook" /> }
</a>
)
}
This approach somewhat works, credentials are loaded and user is redirected to authenticated route but console still throws that error and ui is sometimes flicker between routes. Ive spent last two days on this and im out of ideas. Am i missing something obvious?
Ok, finally figured it out, turns out I shouldn't hijack logic from apollo hooks and be careful of how data is handled. I assume mutation hook updates client state on its own, .then() block resolved before client update and unmounted component. Maybe someone can clarify?
Anyway here is updated code if anyone is interested:
const FaceookSignIn = (props) => {
const appId = '187856148967924'
const redirectUrl = `${document.location.protocol}//${document.location.host}/facebook-callback`
const code = (document.location.pathname === '/facebook-callback') ? querystring.parse(document.location.search)['?code'] : null
//moved data handling logic to hooks optional callback
const [callFacebook, {client, data, loading, error, called}] = useMutation(FACEBOOK_SIGN_IN, {onCompleted: (data)=>{
const {name, email, accessToken} = data.facebookSignIn
client.writeData({
data: {
user: {
name: name,
email: email,
accessToken: accessToken,
__typename: 'User'
}
}
})
}})
if(code && !called) {
callFacebook({variables: {code: code}})
}
const handleClick = e => {
e.preventDefault()
window.location.href = `https://www.facebook.com/v2.9/dialog/oauth?client_id=${appId}&redirect_uri=${encodeURIComponent(redirectUrl)}`;
}
return (
<a className="login-options__link" href='/facebook-login' onClick={handleClick}>
{loading ? <p>loading...</p> : <img className="social-link__icon" src={fb.default} id="facebook" /> }
</a>
)
}

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