React-Router Race Condition - reactjs

I'm trying to implement protected routes. The issue is, the navigate happens before the setSession has updated meaning the authContext is still false and the protected route component sends the user back to /sign-in
This is the handleSubmit function on my sign in form
const handleSubmit =
async (e) => {
e.preventDefault();
let { data, error } =
await auth.signIn({
email,
password
})
if (error)
setAuthError(error.message);
if (data)
navigate('/dashboard');
}
This is the signIn function on my context, called by the function above
async ({ email, password }) => {
let { data, error } =
await client.auth.signInWithPassword({
email,
password
})
if (data)
setSession(data.user)
return { data, error }
},
...and of course the protected route component is essentially
let { isSignedIn } = useContext(AuthContext);
return (
isSignedIn
? children
: <Navigate to="/sign-in" replace />
)
From looking around this seems to be the basic structure that protected route tutorials have: use a handler function to call the sign-in function on a context; context sets some state and returns; the handler function then navigates.
I'm using React-Router 6.8.0. Funnily enough, the sign-in/out button in the nav (which is not under the react router <Outlet/> seems to work)

I think you need to have a separate state for managing when the checking of the auth status is happening. At the the minute your logic doesn't cover that scenario, for example, something like -
let { isSignedIn, isAuthLoading } = useContext(AuthContext);
if(isAuthLoading) return <Loading />
return (
isSignedIn
? children
: <Navigate to="/sign-in" replace />
)
or whatever way you feel best suits your app structure, but essentially, you need to have some way of handling the states that represent when you're either checking if the user is authenticated, or during authentication, then you can use your existing private route logic when you know if the user is logged in or not.

From what I can tell this is due to "batching" - https://github.com/reactwg/react-18/discussions/21, the fix was to use flushSync around the setSession state call.
React applies the setSession call asynchronously, and in this case the state still hasn't been updated by the time the <ProtectedRoute/> component is hit (after we've navigated with navigate('/dashboard')), so isSignedIn is still false so we get sent back to /sign-in.
I'm guessing the majority of those "protected routes" tutorials are using React < 18, where there was no batching of setState calls outside of event handlers i.e inside promises which means they updated synchronously and thus the above pattern worked without the use of flushSync.

Related

How do I avoid duplicate action dispatches caused by to a delay in redux store update/propagation of redux state to connected components?

I'm working on a React app that uses React-Router and Redux.
This app has certain routes that require authentication, in this case I'm redirecting the user to a login page as follows.
return <Redirect
to={{
pathname: '/login',
search: `?ret=${encodeURIComponent(location.pathname)}`
}}
The login page is rendered using a Login component, which is connected to the redux store and checks a boolean isSignedIn. When isSignedIn === true, I redirect the user to the path specified in the ret query param. The relevant code is below.
const Login = (props) => {
if (!props.isSignedIn) {
// show a message and CTA asking the user to log in
}
const queryParams = qs.parse(props.location.search, { ignoreQueryPrefix: true });
const ret = queryParams.ret || '/';
return <Redirect to={ret} />
}
I'm using google oAuth2 in the app, and after the user signs in via Google, I dispatch a SIGN_IN action which updates isSignedIn to true. The oAuth flow is handled by a GoogleAuth component, which checks whether a user is signed in each time a page is rendered, and also handles the sign in/sign out click events. This component renders on every page of the app.
class GoogleAuth extends React.Component {
componentDidMount() {
window.gapi.load('client:auth2', () => {
window.gapi.client.init({
clientId: 'someclientid',
scope: 'email'
}).then(() => {
this.auth = window.gapi.auth2.getAuthInstance();
// initial check to see if user is signed in
this.onAuthChange(this.auth.isSignedIn.get());
// listens for change in google oAuth status
this.auth.isSignedIn.listen(this.onAuthChange);
})
});
};
onAuthChange = async (isGoogleSignedIn) => {
if (!this.props.isSignedIn && isGoogleSignedIn){
await this.props.signIn(this.auth.currentUser.get().getId());
} else if (this.props.isSignedIn && !isGoogleSignedIn) {
await this.props.signOut();
}
}
handleSignIn = () => {
this.auth.signIn();
}
handleSignOut = () => {
this.auth.signOut();
}
The issue I'm facing is that the SIGN_IN action (dispatched by calling this.props.signIn()) is getting called multiple times when I log in from the '/login' page, and get redirected.
It appears that the redirect occurs before the redux store has properly updated the value of isSignedIn and this results in a duplicate SIGN_IN action dispatch.
How can I prevent this? I considered adding a short delay before the redirect, but I'm not sure that's the right way.
EDIT:
I found out that this is happening because I'm rendering the GoogleAuth component twice on the login page (once in the header, and once on the page itself). This resulted in the action getting dispatched twice. Both GoogleAuth components detected the change in authentication status, and as a result both dispatched a SIGN_IN action. There was no delay in propagation of redux store data to the connected component, at least in this scenario.
Ok, figured this out.
I'm rendering the GoogleAuth component twice on the login page - once in the header, and once within the main content. That's why the action was getting dispatched twice.
Both GoogleAuth components detected the change in authentication status, and as a result both dispatched a SIGN_IN action.

Fetch data after login

I have a login component:
const login = async (e) => {
e.preventDefault();
const {data} = await loginUserRes({variables: {
...formData,
}});
if (data) {
currentUserVar({
...data.loginUser,
isLoggedIn: true,
});
}
};
Which allows user to send some data to the server, if the credentials are correct a JWT is stored in a cookie for authentication.
I'm wondering what he best approach is to fetching data after a log in, on the dashboard I have a list of movies that the user has added. Should I use a react effect to react to the currentUserVar changing and then do request for the movies? Or perhaps I could add all the dashboard data to the return object of the apollo server, that way the data is present on login.
Yes you should react to your authorization data, But this info always considered as global state that you will need it in the whole app. so It is recommended to save your currentUserVar in a react context or whatever you are using to handle global state, Even if the app was small and you don't have global state then you can left it to your root component so you can reach this state in all your components.
Here how I handle auth in reactjs, I don't know if it is the better approach but I'm sure it is a very good solution:
Create a wrapper component named AuthRoute that checks if the user is logedin or
not.
Wrap all components that require user auth with AuthRoute
If we have user data then render wrapped component.
If there is no user data then redirect to Login component
const AuthRoute = ({component, children, ...rest}) => {
const Component = component;
const {globalState} = useContext(GlobalContext);
// Return Component if user is authed, else redirect it to login page
return (
<Route
{...rest}
render={() =>
!!globalState.currentUser ? ( // if current user is exsits then its login user
children
) : (
<Redirect
to={ROUTES_PATH.SIGN_IN.index}
/>
)
}
/>
);
};
Then any component can fetch the needed data in it's mount effect useEffect(() => { fetchData() }, []). and this is an indirect react to userData.
Note: if you are not using routing in your app, you still can apply this solution by converting AuathRoute component to HOC.

How can i redirect on submitting my form in reactjs and using formik

the redirect is not working, how can I redirect am using functional components.
const [redirect,setredirect]=useState(false);
const onSubmit = (values,{resetForm}) => {
console.log('Form data', values);
setredirect(true);
if(redirect)
{
return <Redirect to='/' />
}
You can use history.push to redirect.
const history = useHistory()
history.push('/')
Btw, the condition to check if redirect is true won't work since setRedirect is asynchronous. You don't need the redirect piece of state at all.
const onSubmit = async (values) => {
// Send an API request with the values
// await makeAPICall(values)
history.push('/')
};
The problem is that setting the state does not (necessarily) change it synchronously, so if you check the state right after you set it, it may still contain the old value. In react, setting the state does not immediately mutate the state, but creates a pending state transition. If you need to reliably access the new state right after setting it, just save it to a temporary variable:
let shouldRedirect = true; // I'm assuming this is not hard-coded or the if statement makes no sense.
setredirect(shouldRedirect);
if (shouldRedirect) {
// Do something
}

React with react-redux-firebase isLoaded is true and isEmpty is seemingly false, yet firebase.auth().currentUser is null - what could be the cause?

so I might have difficulty explaining this issue I am having, which I am not able to reproduce consistently. I have a React app on which I am using react-redux-firebase and that I thought I was successfully implementing to keep track of the user session.
My App.js file has the following bit or routing code as a sample (using react-router-dom):
<Route
path="/signin"
render={() => {
if (!isLoaded(this.props.auth)) {
return null;
} else if (!isEmpty(this.props.auth)) {
return <Redirect to="/posts" />;
}
return <Signin />;
}}
/>
This works correctly. I go to Signin component when user is not logged in or Posts when user is logged in. In Signin component I have this bit of logic that happens:
// sign the user in
this.props.firebase.login({
email: user.email,
password: user.password
}).then(response => {
// detect the user's geolocation on login and save
if (isLoaded(this.props.auth)) {
const navigator = new Navigator();
navigator.setGeoLocation(this.props.firebase);
}
// redirect user to home or somewhere
this.props.history.push('posts');
})
I am importing isLoaded like so:
import { firebaseConnect, isLoaded } from 'react-redux-firebase';
the condtional works fine, the user is logged in and then the isLoaded conditional happens - this is where the problem arises. With isLoaded true I would assume that the user and all the redux-firestore user properties are ready for use....but that is sometimes not the case. In navigator.setGeoLocation call I have this:
setGeoLocation(propsFirebase, cb) {
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition((position) => {
propsFirebase.auth().currentUser
.getIdToken(/* forceRefresh */ true)
.then((idToken) => {
...
});
}
)
}
}
At this point, propsFirebase.auth().currentUser is sometimes null (this originates in the parameter passed in navigator.setGeoLocation(this.props.firebase);). If I try the signin in all over again, then it works. I am not able to find any consistent way of reproducing.
I have noticed this in other components too. I am not sure if this is an issue that happens when my computer goes to sleep and I should restart the whole React process or what? has anyone seen similar issues? If so, what could I possibly be missing during checking user state in the routing?
If more code is necessary, let me know...
currentUser will be null with the user is not signed in. It will also be null when a page first loads, before the user's token has been loaded and a User object is available. You should use an auth state observer to get the User object if you want to act immediately after it is loaded asynchronously after page load.
firebase.auth().onAuthStateChanged((user) => {
if (user) {
// User is signed in, see docs for a list of available properties
// https://firebase.google.com/docs/reference/js/firebase.User
var uid = user.uid;
// ...
} else {
// User is signed out
// ...
}
});
You might also want to read for more detail: Why is my currentUser == null in Firebase Auth?

React : How to make the SAME component re-render?

I've found myself at a bit of dead end here. I'll try to explain it as best as I can.
I'm using base username routing on my app, meaning that website.com/username will route to username's profile page. Everything works fine apart from one issue that I'll get to below.
This is how I'm doing it (very short version) :
This route looks for a potential username and renders the Distribution component.
<Route exact path="/([0-9a-z_]+)" component={Distribution} />
Distribution component then extracts the potential username from the pathname...
username = {
username: this.props.location.pathname.substring(1)
};
...And then fires that off to my API which checks to see if that username actually exists and belongs to a valid user. If it does it returns a user object, if not it returns an error.
if (username) {
axios
.post(API_URI + '/users/get/profile', JSON.stringify(username))
.then(res => {
this.setState({
ready: true,
data: res.data,
error: ''
});
})
.catch(err => {
this.setState({
ready: true,
data: '',
error: err.response
});
});
}
All of the above is happening inside componentWillMount.
I then pass the relevant state info as props to the relevant child component in the render :
render() {
if (this.state.ready) {
if (this.state.data) {
return <UserProfile profile={this.state.data} />;
}
if (this.state.error) {
return <Redirect to="notfound" />;
}
}
return null;
}
As I mentioned, this all works perfectly when moving between all of the other routes / components, but it fails when the Distribution component is called while already IN the Distribution component. For example, if you are already looking at a valid profile (which is at Distribution > UserProfile), and then try to view another profile, (or any other malformed username route that would throw an error), the API call isn't getting fired again so the state isn't being updated in the Distribution component.
I originally had it all set up with a Redux store but had the exact same problem. I wrongly thought that componentDidMount would be fired every single time the component is called for the first time, and I assumed that throwing a new url at it would cause that but it doesn't.
I've tried a bunch of different ways to make this work (componentWillReceiveProps etc) but I just can't figure it out. Everything I try throws depth errors.
Am I missing a magical piece of the puzzle here or just not seeing something really obvious?
Am I going about this entirely the wrong way?
You on the right path when you tried to use componentWillReceiveProps. I would do something like the following:
componentDidMount() {
this.refresh()
}
componentWillReceiveProps(prevProps) {
if(prevProps.location.pathname !== this.props.location.pathname) {
this.refresh(prevProps)
}
}
refresh = (props) => {
props = props || this.props
// get username from props
// ...
if (username) {
// fetch result from remote
}
}

Resources