Change route in React Native with React Router after an async action - reactjs

I have a few routes setup like so:
import createHistory from 'history/createMemoryHistory';
import { NativeRouter, Route } from 'react-router-native';
<NativeRouter history={createHistory()}>
<View style={styles.container}>
<Route exact path='/' component={Home}/>
<Route path='/page1' component={Page1}/>
</View>
</NativeRouter>
Now it's simple to change routes programmatically in my Home class for example:
this.props.history.push('/page1');
But in the case of an async action that I'm dispatching with Redux:
export function login(email, password) {
return function(dispatch) {
dispatch(setAuthBusy(true));
return auth.signIn(email, password)
.then(function (user) {
dispatch(setAuthBusy(false));
dispatch(setUser(user));
// !CHANGE ROUTE HERE!
})
.catch(function (err) {
dispatch(setAuthBusy(false));
dispatch(setAuthError(err.message));
});
};
}
In this case you can see that it's a login action that authenticates a user and only if the user has been successfully authenticated does the route change to /page1 for instance.
I have a feeling that changing routes in your async actions isn't the correct way to go about it so I'd appreciate some advice in terms of the general architecture and flow of the app. Thanks!

If you start to deal with async actions, you should give a look at redux-thunk middleware.
Using redux-thunk you could do the following :
class Home extends React.Component {
this.onLogin = () => {
dispatch(login('toto', 'toto')).then(() => {
this.props.history.push('/page1');
})
}
}
This mean that the route change in your case could be managed by the component itself.
Hope it help.

Related

Why React batch not working in async callback

I'm using React "version": "18.2.0" which must support batching everywhere(in callback, in setTimeout and in Promises).
I have redux store which looks like this:
const initialState = {
token: null,
};
const accessSlice = createSlice({
name: 'access',
initialState,
reducers: {
setToken: (state, action) => {
state.token = action.payload;
},
},
});
I have react component which renders route(from react-router v5) only if we do not have token in redux store and if we do not have token this component redirects us to root page:
import { Redirect, Route } from 'react-router-dom';
import { tokenSelector } from '../redux/slices/access';
const UnauthorizedRoute = ({ exact, path, children }) => {
// we get token from redux store
const token = useSelector(tokenSelector);
return (
<Route
exact={exact}
path={path}
>
{token ? children : <Redirect to="/" />}
</Route>
};
};
Then we have LoginPage component which we render like this:
<UnauthorizedRoute exact path="/login">
<LoginPage />
</UnauthorizedRoute>
In LoginPage component on form submit I have callback which looks like this:
const handleOnSubmit = async (formValues) => {
const token = await backendApi.getToken(formValues);
dispatch(setToken(token));
history.push('/welcome-page');
}
<LoginForm
onSubmit={handleOnSubmit}
/>
I expect that react will rerender application only once after these code executes
dispatch(setToken(token));
history.push('/welcome-page');
In other words I expect that react will batch these two function calls(first call dispatches redux action and second call redirects us to welcome page). But in reality after executing this code user is redirected to root page '/' because it looks like after executing these dispatch and history.push UnauthorizedRoute component gets new token from redux store and redirects us to root page '/'.
But if change handleSubmit callback like this(we do not go to backend and use fake token):
const handleOnSubmit = async (formValues) => {
dispatch(setToken('faketoken'));
history.push('/welcome-page');
}
<LoginForm
onSubmit={handleOnSubmit}
/>
all works! user is redirected to '/welcome-page' and in redux store token value is 'faketoken'
Maybe somebody knows how to fix it? I tried to wrap these code using unstable_batchedUpdates but it does not help
unstable_batchedUpdates(() => {
dispatch(setToken(token));
history.push('/welcome-page');
});
Not sure why but this code fixed the issue:
import { flushSync } from 'react-dom';
flushSync(() => {
dispatch(setToken(token));
history.push('/welcome-page');
});

Navigate to different page on successful login

App.tsx
import React from 'react';
import {
BrowserRouter as Router,
Redirect,
Route,
Switch
} from 'react-router-dom';
import './app.css';
import Login from './auth/pages/login';
import DashBoard from './dashboard/dashboard';
export const App = () => {
return (
<div className="app">
<Router>
<Switch>
<Route path="/auth/login" component={Login} />
<Route path="/dashboard" component={DashBoard} />
<Redirect from="/" exact to="/auth/login" />
</Switch>
</Router>
</div>
);
};
export default App;
login.tsx
import { useHistory } from 'react-router-dom';
const authHandler = async (email, password) => {
const history = useHistory();
try {
const authService = new AuthService();
await authService
.login({
email,
password
})
.then(() => {
history.push('/dashboard');
});
} catch (err) {
console.log(err);
}
};
From the above code I'm trying to navigate to dashboard on successful login.
The auth handler function is being called once the submit button is clicked.
The login details are successfully got in authhandler function, but once I use history to navigate I get the following error
"Uncaught (in promise) Error: Invalid hook call. Hooks can only be called inside of the body of a function component"
Error text is pretty clear. You can not call useHistory, or any other hook, outside of functional component. Also, hooks must be called unconditionally, on top of component. Try to call useHistory inside your actual component and pass history as a parameter to authHandler.
The problem is that authHandler is an async function and using a hook inside a "normal" function don't work. It breaks the rule of hooks.
What you need to do is separate authHandler and history.push('/dashboard').
What you can do is return the async request and use .then to call history.push.
const authHandler = async (email, password) => {
const authService = new AuthService();
// returning the request
return await authService
.login({
email,
password
})
};
And inside your component you use the useHistory hook and call authHandler on some action.
const MyComponent = () => {
const history = useHistory()
const onClick = (email, password) => {
authHandler(email, password)
.then(() => history.push('/dashboard'))
}
return (...)
}

How to async/await on firebase.auth().currentUser?

I am using ReactJs, and defined a Route which will load <Loans />component if the path is mywebsite.com/loans. Below is the code snippet for the <Loans />component. In the componentDidMount, I have async/await to get the currentUser from firebase. If user is null, page will be redirected to /signin page.
class Loans extends Component {
componentDidMount = async () => {
const user = await firebase.auth().currentUser;
if (!user) {
this.props.history.push("/signin");
}
};
render () {
...}
}
Here is the code snippet for <SignIn />component. In SignIn component, there is a listener to listen any auth state change, if user is logged in, page will be redirected to /loanspage.
class SignIn extends React.Component {
componentDidMount() {
firebase.auth().onAuthStateChanged(user => {
if (user) {
this.props.history.push("/loans");
}
});
}
render () {
...
}
}
I actually already logged in. But I observed a weird behavior that whenever I refreshed the page /loans, page will be redirected to /signin page for less than a second and then quickly redirected back to /loans page.
My question is if I already have firebase.auth().currentUser to be async/await, how could I still get null for the user in <Loans /> component, and I only see <Loans /> component when the page is redirected from <SignIn /> page? How can I aviod to see the SignIn page if I already have user logged in in my case. Thanks!
firebase.auth().currentUser isn't a promise, it's a User object, so using await on it doesn't make much sense. According to the API documentation, it's only going to be a User object, or null. It will be null when there is no user signed in, or the User object just isn't available yet.
What you should be doing instead is using the same style of listener in SignIn to determine when a User object is ready, and render any content only after that listener indicates a User is present.
I recently built a HOC to handle this. Quick example below
import React from "react"
import { useAuthState } from "react-firebase-hooks/auth"
import { auth } from "./firebase" // Where to store firebase logic
import { Navigate } from "react-router-dom"
export const RequireAuth = ({ children }: { children: JSX.Element }) => {
const [user, loading] = useAuthState(auth)
if (loading) {
return <></>
} else if (user?.uid && !loading) {
return children
} else {
return <Navigate to="/login" />
}
}
Then in the router (I'm using RR v6) you just wrap the page component in the hoc.
<Route
path="/"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>
You could also extract this out to a hook and call it in every page instead of at the router level but I feel like this is a bit more readable as far as seeing which routes are protected. This also follows the example of protected routes in the RR docs.

How to redirect in axios?

Can I redirect in .then instead of console.log?
axios
.post('/api/users/login', user)
.then(res => console.log(res.data))
.catch(err => this.setState({ errors: err.response.data }))
Is it possible and if it is how can I do it?
I would suggest that you should not perform side operations inside the actions (in this case axios call). This is because you would eventually add these calls inside redux thunk/saga or some other middleware.
The pattern which I follow is you return the promise and add the redirection logic inside your components(or containers). Following are the benefits of it:-
It makes your code cleaner and avoids side affects inside your API calls.
It makes easier to test your API calls using mock data.
Showing alert in case the API fails without passing callback as function parameter.
Now, that being said you can use the above logic in following ways:-
// Component code
import { browserHistory } from 'react-router';
class TestContainer extends React.Component {
auth = () => {
login()
.then(() => {
browserHistory.push("/addUser");
})
.catch(() => {
// Show alert to user;
})
}
render () {
return (
<div>
<button onClick={this.auth}>Auth</button>
</div>
);
}
}
// Action code
const login = () => {
return axios
.post('/api/users/login', user);
}
U can use: location.window.href
Usually I use react-router to redirect
history.js
import createHistory from 'history/createBrowserHistory';
export default createHistory()
Root.jsx
import { Router, Route } from 'react-router-dom'
import history from './history'
<Router history={history}>
<Route path="/test" component={Test}/>
</Router>
another_file.js
import history from './history'
history.push('/test') // this should change the url and re-render Test component
Ref: https://github.com/ReactTraining/react-router/issues/3498
Your question is not about reactjs or axios, its just pure javascript.
Use location.href

redux-react-route v4/v5 push() is not working inside thunk action

In index.js push directly or throw dispatch works well:
...
import { push } from 'react-router-redux'
const browserHistory = createBrowserHistory()
export const store = createStore(
rootReducer,
applyMiddleware(thunkMiddleware, routerMiddleware(browserHistory))
)
// in v5 this line is deprecated
export const history = syncHistoryWithStore(browserHistory, store)
history.push('/any') // works well
store.dispatch(push('/any')) // works well
ReactDOM.render((
<Provider store={store}>
<Router history={history}>
<App />
</Router>
</Provider>
), document.getElementById('root'))
App.js
class App extends Component {
render() {
return (
<div className="app">
<Switch>
<Route path="/" component={Main} />
<Route path="/any" component={Any} />
</Switch>
</div>
);
}
}
export default withRouter(connect(/*...*/)(App))
but in redux-thunk action all attempts ends by rewriting url, but without re-rendering
...
export function myAction(){
return (dispatch) => {
// fetch something and then I want to redirect...
history.push('/any') // change url but not re-render
dispatch(push('/any')) // change url but not re-render
store.dispatch(push('/any')) // change url but not re-render
}
}
This myAction is calling fetch() inside and should redirect after success.
If I run this.props.history.push('/any') inside component, it works! but I need to run redirect inside thunk action after successful fetch()
I was trying wrap all components with withRouter or Route, but didn't help.
Inject history object into your component and use push like this:
import { withRouter } from 'react-router-dom'
import { connect } from 'react-redux'
#withRouter
#connect(({auth})=>({auth}))
class App extends Component {
// on redux state change
componentWillReceiveProps(nextProps) {
if(!nextProps.auth)
this.props.history.push('/login')
}
render() {
return (
<div>
<Button
// on button click
onClick={this.props.history.push('/')}
>
Home page
</Button>
</div>
);
}
}
I made workaround by delegating state of successfull fetch() to the component (thanks #oklas) where is history.push() or <Redirect> working:
{this.props.fetchSuccessfull && <Redirect to="/any" />}
But still waiting for better solution by calling push() directly from thunk action.
Well, let me then submit another not perfect solution by passing the history object in the dispatch to the action. I guess it's more a beginners-solution but is IMHO simple to understand (and therefore simple to maintain which is the most important thing in software-development)
Using <BrowserRouter> makes all React-compoments having the history in their props. Very convenient. But, as the problem description stated, you want it outside a React Component, like an action on Redux-Thunk.
Instead of going back to <Router> I chose to stick to BrowserRouter.
The history object cannot be accessed outside React Components
I did not like going back to <Router> and using something like react-router-redux
Only option left is to pass along the history object to the action.
In a Auth-ForgotPassword component:
const submitHandler = (data) => {
dispatch(authActions.forgotpassword({data, history:props.history}));
}
In the action function
export const forgotpassword = ({forgotpasswordData, history}) => {
return async dispatch => {
const url = settings.api.hostname + 'auth/forgotpassword'; // Go to the API
const responseData = await fetch(
url,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
body: JSON.stringify(forgotpasswordData),
}
);
history.push('/auth/forgotpassword/success');
}
}
And now we all wait for the final elegant solution :-)

Resources