React router 4 history push component re render - reactjs

With redux-saga and react router 4. I am trying to implement a flow for user registration. I am focusing on the part where user is presented a registration screen on /user/register route.
GOAL
Goal is to update the status of user registration on this same screen as an Alert depending upon either the user created successfully or there already exists a user. I am using redux-saga and using history.push from saga to update the view.
PROBLEM
The alert is shown but only after I reload the /user/register page.
I am passing state to history.push from my saga and then in my component based on that state which I extract from this.props.location.state I prepare the content for alert.
Register Component
// Form submission handler
handleUserRegistration = user => {
this.props.registerUser(user, this.props.history);
}
// Prepring the alert content
getAlertUI = signupState => {
if (signupState.signupSuccess) {
return <UncontrolledAlert color='success'>{'Verification email sent. Please verify your account.'}</UncontrolledAlert>
}else {
return <UncontrolledAlert color='danger'>{signupState.error.message}</UncontrolledAlert>
}
}
render () {
let alertContent = null;
const signupResponse = this.props.location.state;
if (signupResponse) {
if (signupResponse.error) {
alertContent = this.getAlertUI({signupSuccess: false, error: signupResponse.error});
}else {
if (signupResponse.verificationEmailSent) {
alertContent = this.getAlertUI({signupSuccess: true})
}
}
}
return (
<div> {alertContent} </div>
// My form component goes here.
)
}
While is my saga. I am using history.push with the necessary information.
saga.js
const registerWithEmailPasswordAsync = async (userData) =>
await axios.post(apiUrls.SINGUP_USER, userData )
.then(response => {
return {
isError: false,
data: response.data,
}
})
.catch(error => {
return {
isError: true,
errorDetails: {
status: error.response ? error.response.status : null,
message: error.response ? error.response.data : null,
}
}
})
function* registerUser({ payload }) {
const { history } = payload;
try {
const registerUser = yield call(registerWithEmailPasswordAsync, payload.user);
if (!registerUser.isError) {
history.push('/user/register', {verificationEmailSent: true});
} else {
if (registerUser.errorDetails) {
history.push('/user/register', {error: registerUser.errorDetails} );
}
}
} catch (error) {
console.log('register error : ', error)
}
}
I am pretty new to this, Please share if this is the better approach or not? And if it is why isn't it updating my view. Any pointers are highly appreciated.

Don't use history.push just to update state
The only reason I could see you using history.push from within the saga is for sending the user to a dashboard/verification page after a successful sign up. There is no reason to use history.push just to pass some state to the component dynamically. Update your redux state by dispatching an action, not using history.push
You don't necessarily have to update state to notify the user of a sign-up error
You asked if your general approach was the best. Personally, I don't think it is. I wouldn't work that hard. Just use something like react-toastify. You can then just place a ToastContainer in your App component, then call toastify(successMessage/error)

Related

i cant redirect after getting a response using axios post request in react i tried using Redirect but not able to do it

So I am trying to redirect using react-router-dom component Redirect but I am not able to do it I dont understand why . Pls help me I am a beginner and cant understand what's wrong is happening clearly with the code.
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await SearchAction(values);
console.log(res.data);
setResult(res.data); //this is the response
<Redirect to={{ pathname: '/search/result', state: { data: `${res.data}` } }} />; //this is the redirect
} catch (err) {
console.log('error searching', err);
}
};
Problem: Improper Placement of <Redirect/>
Your code is attempting to render a Redirect component inside of an event handler. This doesn't make sense and won't work. Event handlers can modify your state but they can't render anything directly.
Solution: Conditionally Render <Redirect/>
You should render the Redirect conditionally based on the current state of your component. The Redirect needs to the returned in the correct place, which is your component's return section (or render() for a class component). You want to render the Redirect only after the event handler has set its data. Assuming that your result state is initially undefined, we can check the value of result and see if it has been updated with data.
function MyComponent() {
const [result, setResult] = useState();
const handleSubmit = async (e) => {
e.preventDefault();
try {
const res = await SearchAction(values);
console.log(res.data);
setResult(res.data); //this is the response
} catch (err) {
console.log("error searching", err);
}
};
return (
<div>
{!!result && (
<Redirect
to={{ pathname: "/search/result", state: { data: result } }}
/>
)}
/* ... */
</div>
);
}
Passing Through Data
It is not the best idea to pass large amounts of data through the Redirect. In my (very biased) opinion, querying the API should be the job of the search results page. It would be better to just pass the values to your "/search/result" page and move the API request into that component.

React login using context API with private route

In my react web app, login-logout functionality is implemented using context-API and hooks. Everything seems to work fine except for the fact that I have to click the 'Login' button twice to push the user to the dashboard.
The statement, return <Redirect to="/dashboard /> does not work, instead I have to use history.push('/dashboard'), which does not seems to be a better option to me while login.
Here is the working snippet :
https://codesandbox.io/s/wizardly-worker-mqkex?file=/src/AuthContext.js
Also, I need some suggestions for the best practise to fetch the logged user details in other components. Using localstorage or global context API state, which of them serves as the best option for the same ?
Any help to resolve this, appreciated :)
Well, it boils down to the simple fact that your context is not updated when you do your check. The simplest solution would be to remove the check for isLoggedIn and push the user to the Dashboard:
const postLogin = async (e) => {
e.preventDefault()
try {
await login()
props.history.push('/dashboard')
} catch (error) {
dispatch({
type: 'LOGIN_FAILURE',
payload: 'Unable to login'
})
}
}
Then throw an error in your login function when you don't get a 200 back:
const login = async () => {
const loginUser = { status: 200, id: '3654634565659abhdgh' }
if (loginUser.status === 200) {
dispatch({
type: 'LOGIN_SUCCESS',
payload: loginUser.id
})
} else {
throw new Error("Invalid Credentials")
}
}
As your login code is in a try catch block, the user won't get pushed to the Dashboard when the login fails.

async/await in react with redux

Hi guys I am quite new to programming and trying to understand one thing with redux and await/async functions. Basically I have this function:
//nh functions
const onSubmit = async data => {
try{
await dispatch(Login(data))
if (auth.logged != false){
addToast(content, { appearance: 'success', autoDismiss: true, })
history.push('/')
} else if (auth.logged == false){
addToast(content2, { appearance: 'error', autoDismiss: true, })
}
}finally{
console.log('Tada')
}
}
which should first authenticate an account and then push a notification. However, the await is not working at all, and it proceeds immediately to the if statement. Any tips?
Wht dave said is true. If you want to do something like that, you should dispatch your data and get the result in a props. Then you can use a useEffect to listen to this prop and do your things. Somethink like:
useEffect(() => {
// Do your things after your dispatch here
}, [yourProp]);
The "normal" pattern is to write asynchronous pseudo-actions: async function dispatching classical synchronous actions :
// reducer
// define 3 synchronous actions : loginActionBegin, loginActionSuccess,
// loginActionFailure which update your state at your convenience, for example setting a boolean flag to inform components that request is
// flying, or add user infos in store when loginActionSuccess is dispatched...
// async pseudo action
export const loginActionAsync = (loginInfos: any): any => {
return (dispatch: Dispatch, getState: any): any => {
dispatch(loginActionBegin());
return loginService.login(loginInfos)
.then(result: any): any => {
// request succeeded, add toast, user feedback...
dispatch(loginActionSuccess(result));
})
.catch((error: any) => {
// request failed, add toast, user feedback...
dispatch(loginActionFailure(error));
});
};
}
Then in a component:
// grab infos from store
const user = useSelector(state => state.user)
// on login form submit
dispatch(loginActionAsync({username:..., password:...}));
You will need async middleware to do so like https://github.com/reduxjs/redux-thunk
See :
https://redux.js.org/advanced/async-actions
https://www.digitalocean.com/community/tutorials/redux-redux-thunk
https://redux-toolkit.js.org/usage/usage-with-typescript#createasyncthunk
I'm assuming that auth is a prop that is mapped from the redux state to the component?
If so you could get around having to create an async functionality for the submit button and handling the redirect and/or state change when the component is updated with a new value from the store.
I would recommend this as you should rather be handling any error in the action itself, that way the component can be kept simple and mainly focuses on what to display.
useEffect(() => {
if (auth.logged != false){
addToast(content, { appearance: 'success', autoDismiss: true, })
history.push('/')
}
else if (auth.logged == false){
addToast(content2, { appearance: 'error', autoDismiss: true, })
}
}, [auth.logged]);
const onSubmit = data => dispatch(Login(data))

React Mobx componentDidUpdate is not updating when observable changes

I'm new to Mobx and reactjs in general, I have knowledge in Redux and react native, and in Redux when I used to call an action and the props get updated, the componentDidUpdate life cycle method is triggered.
The scenario I'm having now is login. so the user fills the form, clicks submit, and the submit calls a Mobx action (asynchronous), and when the server responds, an observable is updated, and then it navigates to a main page (navigation happens in the component).
Here is my store code.
import { autorun, observable, action, runInAction, computed, useStrict } from 'mobx';
useStrict(true);
class LoginStore {
#observable authenticated = false;
#observable token = '';
#computed get isAuthenticated() { return this.authenticated; }
#action login = async (credentials) => {
const res = await window.swaggerClient.Auth.login(credentials)l
// checking response for erros
runInAction(() => {
this.token = res.obj.token;
this.authenticated = true;
});
}
}
const store = new LoginStore();
export default store;
export { LoginStore };
and this handler is in my component.
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
this.props.store.login(values);
}
});
}
componentDidUpdate() {
if (this.props.store.isAuthenticated) {
const cookies = new Cookies();
cookies.set('_cookie_name', this.props.store.token);
this.props.history.push('main');
}
}
It's not the ideal code, I'm just experimenting, but I'm not quite getting it.
Also, if I use the computed value (isAuthenticated) in the render life cycle method, the componentDidUpdate is triggered, but if I didn't use it in the render method, the componentDidUpdate is not triggered.
For example, if I do this
render() {
if (this.props.store.isAuthenticated) return null
// .... rest of the code here
}
the above will trigger the componentDidUpdate.
Am I missing something? is there a better way to do it with Mobx?
Thanks
Observer component will only react to observables referred in its render method. MobX documentation covers this.
I would recommend you to use when to solve the problem.
componentDidMount() {
when(
() => this.props.store.isAuthenticated,
() => {
// put your navigation logic here
}
);
}
Mobx suggest the following solutions for such a case:
when
autorun
reaction
See the examples below, and don't forget to dispose:
componentDidMount() {
this.disposers.push(
// option with autorun:
autorun(() => {
this.runYourLogicHere();
})
// another option with reaction:
reaction(
() => this.yourModelOrProps.something,
() => {
this.runYourLogicHere();
}
)
)
}
...
componentWillUnmount() {
this.disposers.forEach(disposer => {
disposer();
});
}
And see the answer of #Dominik Serafin in parallel thread as a reference.

React-Router: how to wait for an async action before route transition

Is it possible to call an async redux action known as a thunk on a particular route and not perform the transition until the response has succeeded or failed?
Use Case
We need to load data from the server and fill a form with initial values. These initial values don't exist until the data is fetched from the server.
some syntax like this would be great:
<Route path="/myForm" component={App} async={dispatch(loadInitialFormValues(formId))}>
To answer the original question of preventing the transition to a new route until a response has succeeded or failed:
Because you're using redux thunk you could have the success or failure in the action creator trigger the redirect. I don't know what your specific action / action creator looks like but something like this could work:
import { browserHistory } from 'react-router'
export function loadInitialFormValues(formId) {
return function(dispatch) {
// hit the API with some function and return a promise:
loadInitialValuesReturnPromise(formId)
.then(response => {
// If request is good update state with fetched data
dispatch({ type: UPDATE_FORM_STATE, payload: response });
// - redirect to the your form
browserHistory.push('/myForm');
})
.catch(() => {
// If request is bad...
// do whatever you want here, or redirect
browserHistory.push('/myForm')
});
}
}
Follow up. Common pattern of loading data on entering a route / on componentWillMount of a component and displaying a spinner:
From the redux docs on async actions http://redux.js.org/docs/advanced/AsyncActions.html
An action informing the reducers that the request began.
The reducers may handle this action by toggling an isFetching flag in
the state. This way the UI knows it’s time to show a spinner.
An action informing the reducers that the request finished successfully.
The reducers may handle this action by merging the new data into the
state they manage and resetting isFetching. The UI would hide the
spinner, and display the fetched data.
An action informing the reducers that the request failed.
The reducers may handle this action by resetting isFetching.
Additionally, some reducers may want to store the error message so the
UI can display it.
I followed this general pattern below using your situation as a rough guideline. You do not have to use promises
// action creator:
export function fetchFormData(formId) {
return dispatch => {
// an action to signal the beginning of your request
// this is what eventually triggers the displaying of the spinner
dispatch({ type: FETCH_FORM_DATA_REQUEST })
// (axios is just a promise based HTTP library)
axios.get(`/formdata/${formId}`)
.then(formData => {
// on successful fetch, update your state with the new form data
// you can also turn these into their own action creators and dispatch the invoked function instead
dispatch({ type: actions.FETCH_FORM_DATA_SUCCESS, payload: formData })
})
.catch(error => {
// on error, do whatever is best for your use case
dispatch({ type: actions.FETCH_FORM_DATA_ERROR, payload: error })
})
}
}
// reducer
const INITIAL_STATE = {
formData: {},
error: {},
fetching: false
}
export default function(state = INITIAL_STATE, action) {
switch(action.type) {
case FETCH_FORM_DATA_REQUEST:
// when dispatch the 'request' action, toggle fetching to true
return Object.assign({}, state, { fetching: true })
case FETCH_FORM_DATA_SUCCESS:
return Object.assign({}, state, {
fetching: false,
formData: action.payload
})
case FETCH_FORM_DATA_ERROR:
return Object.assign({}, state, {
fetching: false,
error: action.payload
})
}
}
// route can look something like this to access the formId in the URL if you want
// I use this URL param in the component below but you can access this ID anyway you want:
<Route path="/myForm/:formId" component={SomeForm} />
// form component
class SomeForm extends Component {
componentWillMount() {
// get formId from route params
const formId = this.props.params.formId
this.props.fetchFormData(formId)
}
// in render just check if the fetching process is happening to know when to display the spinner
// this could also be abstracted out into another method and run like so: {this.showFormOrSpinner.call(this)}
render() {
return (
<div className="some-form">
{this.props.fetching ?
<img src="./assets/spinner.gif" alt="loading spinner" /> :
<FormComponent formData={this.props.formData} />
}
</div>
)
}
}
function mapStateToProps(state) {
return {
fetching: state.form.fetching,
formData: state.form.formData,
error: state.form.error
}
}
export default connect(mapStateToProps, { fetchFormData })(SomeForm)
I made a handy hook for this purpose, works with react-router v5:
/*
* Return truthy if you wish to block. Empty return or false will not block
*/
export const useBlock = func => {
const { block, push, location } = useHistory()
const lastLocation = useRef()
const funcRef = useRef()
funcRef.current = func
useEffect(() => {
if (location === lastLocation.current || !funcRef.current)
return
lastLocation.current = location
const unblock = block((location, action) => {
const doBlock = async () => {
if (!(await funcRef.current(location, action))) {
unblock()
push(location)
}
}
doBlock()
return false
})
}, [location, block, push])
}
Inside your component, use it like:
const MyComponent = () => {
useBlock(async location => await fetchShouldBlock(location))
return <span>Hello</span>
}
Navigation will not occur until the async function returns; you can completely block the navigation by returning true.
First and foremost, I want to say that there is a debate around the topic of fetching data with react-router's onEnter hooks whether or not is good practice, nevertheless this is how something like that would go:
You can pass the redux-store to your Router. Let the following be your Root component, where Router is mounted:
...
import routes from 'routes-location';
class Root extends React.Component {
render() {
const { store, history } = this.props;
return (
<Provider store={store}>
<Router history={history}>
{ routes(store) }
</Router>
</Provider>
);
}
}
...
And your routes will be something like:
import ...
...
const fetchData = (store) => {
return (nextState, transition, callback) => {
const { dispatch, getState } = store;
const { loaded } = getState().myCoolReduxStore;
// loaded is a key from my store that I put true when data has loaded
if (!loaded) {
// no data, dispatch action to get it
dispatch(getDataAction())
.then((data) => {
callback();
})
.catch((error) => {
// maybe it failed because of 403 forbitten, we can use tranition to redirect.
// what's in state will come as props to the component `/forbitten` will mount.
transition({
pathname: '/forbitten',
state: { error: error }
});
callback();
});
} else {
// we already have the data loaded, let router continue its transition to the route
callback();
}
}
};
export default (store) => {
return (
<Route path="/" component={App}>
<Route path="myPage" name="My Page" component={MyPage} onEnter={fetchData(store)} />
<Route path="forbitten" name="403" component={PageForbitten} />
<Route path="*" name="404" component={PageNotFound} />
</Route>
);
};
Please notice that your router file is exporting a thunk with your store as argument, if you look upwards, see how we invoked the router, we pass the store object to it.
Sadly, at the time of writing react-router docs return 404 to me, thus I cannot point you to the docs where (nextState, transition, callback) are described. But, about those, from my memory:
nextState describes the route react-router will transition to;
transition function to preform maybe another transition than the one from nextState;
callback will trigger your route transition to finish.
Another think to point out is that with redux-thunk, your dispatch action can return a promise, check it in the docs here. You can find here a good example on how to configure your redux store with redux-thunk.

Resources