I develop a website with React/Redux and I use a thunk middleware to call my API. My problem concerns redirections after actions.
I really do not know how and where I can do the redirection: in my action, in the reducer, in my component, … ?
My action looks like this:
export function deleteItem(id) {
return {
[CALL_API]: {
endpoint: `item/${id}`,
method: 'DELETE',
types: [DELETE_ITEM_REQUEST, DELETE_ITEM_SUCCESS, DELETE_ITEM_FAILURE]
},
id
};
}
react-redux is already implemented on my website and I know that I can do as below, but I do not want to redirect the use if the request failed:
router.push('/items');
Thanks!
Definitely do not redirect from your reducers since they should be side effect free.
It looks like you're using api-redux-middleware, which I believe does not have a success/failure/completion callback, which I think would be a pretty useful feature for the library.
In this question from the middleware's repo, the repo owner suggests something like this:
// Assuming you are using react-router version < 4.0
import { browserHistory } from 'react-router';
export function deleteItem(id) {
return {
[CALL_API]: {
endpoint: `item/${id}`,
method: 'DELETE',
types: [
DELETE_ITEM_REQUEST,
{
type: DELETE_ITEM_SUCCESS,
payload: (action, state, res) => {
return res.json().then(json => {
browserHistory.push('/your-route');
return json;
});
},
},
DELETE_ITEM_FAILURE
]
},
id
}
};
I personally prefer to have a flag in my connected component's props that if true, would route to the page that I want. I would set up the componentWillReceiveProps like so:
componentWillReceiveProps(nextProps) {
if (nextProps.foo.isDeleted) {
this.props.router.push('/your-route');
}
}
The simplest solution
You can use react-router-dom version *5+ it is actually built on top of react-router core.
Usage:
You need to import useHistory hook from react-router-dom and directly pass it in your actions creator function call.
Import and Creating object
import { useHistory } from "react-router-dom";
const history = useHistory();
Action Call in Component:
dispatch(actionName(data, history));
Action Creator
Now, you can access history object as a function argument in action creator.
function actionName(data, history) {}
Usually the better practice is to redirect in the component like this:
render(){
if(requestFullfilled){
router.push('/item')
}
else{
return(
<MyComponent />
)
}
}
In the Redux scope must be used react-redux-router push action, instead of browserHistory.push
import { push } from 'react-router-redux'
store.dispatch(push('/your-route'))
I would love not to redirect but just change the state. You may just omit the result of deleted item id:
// dispatch an action after item is deleted
dispatch({ type: ITEM_DELETED, payload: id })
// reducer
case ITEM_DELETED:
return { items: state.items.filter((_, i) => i !== action.payload) }
Another way is to create a redirect function that takes in your redirect Url:
const redirect = redirectUrl => {
window.location = redirectUrl;
};
Then import it into your action after dispatch and return it;
return redirect('/the-url-you-want-to-redirect-to');
import { useHistory } from "react-router-dom";
import {useEffect} from 'react';
const history = useHistory();
useEffect(()=>{
// change if the user has successfully logged in and redirect to home
//screen
if(state.isLoggedIn){
history.push('/home');
}
// add this in order to listen to state/store changes in the UI
}, [state.isLoggedIn])
A simple solution would be to check for a state change in componentDidUpdate. Once your state has updated as a result of a successful redux action, you can compare the new props to the old and redirect if needed.
For example, you dispatch an action changing the value of itemOfInterest in your redux state. Your component receives the new state in its props, and you compare the new value to the old. If they are not equal, push the new route.
componentDidUpdate(prevProps: ComponentProps) {
if (this.props.itemOfInterest !== prevProps.itemOfInterest) {
this.props.history.push('/');
}
}
In your case of deleting an item, if the items are stored in an array, you can compare the new array to the old.
Related
I'm using react-redux & redux-thunk for my project.
I have to inject my actions to a component by using connect.
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])
My task is one level up. I don't want just to inject multiple actions in this form:
{
doThis(),
doThat()
}
But in this form:
{
this: {
doThis1(),
doThis2()
}
that: {
doThat()
}
}
So basically my problem is that I want to dispatch multiple action-creator files because I want them organized as such.
I tried this version which obviously doesn't work because dispatch is not injected in each Thunk Action Creator:
import * as actions from './actions'
const mapDispatchToProps = (dispatch) => {
return {
dataActions: {
...actions.dataActions
}
};
}
export default connect(null, mapDispatchToProps)(Component);
So my final question is:
Am I even supposed to use Redux this way? Can I organize my files this way, if so how?
If instead of having one property per action creator, you want to structure your bound action creators in a couple of properties that each contain a group of action creators, you can do something like this:
import { bindActionCreators, .. } from 'redux';
..
const mapDispatchToProps = (dispatch) => {
return {
dataActions: bindActionCreators(actions.dataActions, dispatch),
otherActions: bindActionCreators(actions.otherActions, dispatch),
..
};
};
The first argument to bindActionCreators is an object containing action-creator functions (e.g. an imported module that exports only action creators). In your actual component, you should then be able to use this.props.dataActions.someDataAction(..).
If the question is just about whether you can keep different action creators in different files, you might not even want to group the action creators and just do this:
return {
...bindActionCreators(actionCreatorsFromOneModule, dispatch),
...bindActionCreators(actionCreatorsFromAnotherModule, dispatch),
..
};
I am facing a tricky issue with asynchronicity in an angular 2 app.
I am basically trying to rehydrate/reload information from the backend when the app is reloaded/bootstrapped in the user's browser (think F5/refresh). The issue is that before the backend async method returns the result, a router guard is called and blocks...
I reload the information from the root component's ngOnInit method as follows:
from root component:
ngOnInit() {
//reloadPersonalInfo returns an Observable
this.sessionService.reloadPersonalInfo()
.subscribe();
}
from sessionService:
reloadPersonalInfo() {
//FIRST: the app execution flow gets here
let someCondition: boolean = JSON.parse(localStorage.getItem('someCondition'));
if (someCondition) {
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_AUTHENTICATED});
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
return Observable.empty();
}
The trouble is that I have a router CanActivate guard as follows:
canActivate() {
//SECOND: then the execution flow get here and because reloadPersonalInfo has not completed yet, isAuthenticated will return false and the guard will block and redirect to '/signin'
const isAuthenticated = this.sessionService.isAuthenticated();
if (!isAuthenticated) {
this.router.navigate(['/signin']);
}
return isAuthenticated;
}
isAuthenticated method from sessionService:
isAuthenticated(): boolean {
let isAuthenticated = false;
this.store.select(s => s.authenticated)
.subscribe(authenticated => isAuthenticated = authenticated);
return isAuthenticated;
}
So to recap:
FIRST: the reloadPersonalInfo method on sessionService is called by root component ngOnInit. The execution flow enters this method.
SECOND: in the meantime, the router guard is called and sees that the state of authenticated is false (because reloadPersonalInfo has not completed yet and therefore not set the authenticated state to true.
THIRD: reloadPersonalInfo completes too late and sets the authenticated state to true (but the router guard has already blocked).
Can anyone please help?
edit 1: Let me stress that the authenticated state that matters is the one in the store; it is set by this line: this.store.dispatch({type: SET_AUTHENTICATED});.
edit 2: I changed the condition from authenticated to someCondition in order to reduce confusion. Previously, there was another state variable called authenticated...
edit 3: I have changed the isAuthenticated() method return type to Observable<boolean> instead of boolean (to follow Martin's advice) and adapted the canActivate method as follows:
canActivate() {
return this.sessionService.isAuthenticated().map(isAuthenticated => {
if (isAuthenticated) {
return true;
}
this.router.navigate(['/signin']);
return false;
});
}
from sessionService:
isAuthenticated(): Observable<boolean> {
return this.store.select(s => s.authenticated);
}
It makes no difference unfortunately...
Can someone please advise as to how to sort this asynchronicity issue?
There should be two possible ways to solve this:
Solution 1
The quickest, would be to distinguish your isAuthenticated not into 2 but 3 states, this way you can encode one more cruical piece of information into the state: At the time of bootstrapping(when no response from the server has been received), there is no way the client can know for sure if its credentials/tokens are valid or not, thus the state should correctly be "unknown".
First you have to change the initial state of authenticated in your store to null (you also may choose undefined or even use numbers depending on your personal taste). And then you just have to add a .filter to your guard, that renders the guard practically "motionless":
canActivate() {
return this.sessionService.isAuthenticated()
.filter(isAuthenticated => isAuthenticated !== null) // this will cause the guard to halt until the isAuthenticated-question/request has been resolved
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
Solution 2
The second solution would be very similar, but instead of encoding a 3rd state into authenticated you'd add a new flag to your store called authRequestRunning, that is set to true while the auth-request is being made, and set to false after it completes. Your guard would then look only slightly different:
canActivate() {
return this.sessionService.isAuthenticated()
.switchMap(isAuth => this.sessionService.isAuthRequestRunning()
.filter(running => !running) // don't emit any data, while the request is running
.mapTo(isAuth);
)
.do(isAuth => (if (!isAuth) {this.router.navigate(['/signin'])}));
}
With solution #2 you might have some more code. and you have to be careful that the authRequestRunning is set to false first before the authenticated-state is updated.
Edit: I have edited the code in solution #2, so the order of setting the running-status and the auth-status does not matter any more.
The reason why I would use solution #2 is, because in most cases such a state-flag already exists and is being used to display a loading-indicator or something like that.
canActivate itself can return an Observable.
Instead of returning the boolean result in canActivate, return the isAuthenticated Observable.
why are you not setting Authenticated before you give a retrieveCurrentUserAccount call? IT seems you already know if your user is authenticated or not based upon the value inside localStorage
if (isAuthenticated) {
// set before you give a async call.
this.store.dispatch({type: SET_AUTHENTICATED});
return this.userAccountService.retrieveCurrentUserAccount()
.switchMap(currentUserAccount => {
//THIRD: and finally, the execution flow will get there and set the authenticated state to true (unfortunately too late)...
this.store.dispatch({type: SET_CURRENT_USER_ACCOUNT, payload: currentUserAccount});
return Observable.of('');
});
}
Update
Try below,
import { Component, Injectable } from '#angular/core';
import { Router, Routes, RouterModule, CanActivate } from '#angular/router';
import { Subject } from 'rxjs/Subject';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
#Injectable()
export class SessionService{
private _isAuthenticated: Subject<boolean> = new Subject<boolean>();
public get isAuthenticated(){
return this._isAuthenticated.asObservable();
}
reloadPersonalInfo(){
setTimeout(() => {
this._isAuthenticated.next(true);
// do something else too...
}, 2000);
}
}
#Component({
selector: 'my-app',
template: `<h3>Angular CanActivate observable</h3>
<hr />
<router-outlet></router-outlet>
`
})
export class AppComponent {
constructor(private router: Router,
private sessionService : SessionService) { }
ngOnInit() {
this.sessionService.reloadPersonalInfo();
}
}
#Component({
template: '<h3>Dashboard</h3>'
})
export class DashboardComponent { }
#Component({
template: '<h3>Login</h3>'
})
export class LoginComponent { }
#Injectable()
export class DashboardAuthGuard implements CanActivate {
constructor(private router: Router, private sessionService : SessionService) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot){
return this.sessionService.isAuthenticated.map(res => {
if(res){
return true;
}
this.router.navigate(['login']);
}).take(1);
}
}
let routes: Routes = [
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full'
},
{
path: 'dashboard',
canActivate: [DashboardAuthGuard],
component: DashboardComponent
},
{
path: 'login',
component: LoginComponent
}
]
export const APP_ROUTER_PROVIDERS = [
DashboardAuthGuard
];
export const routing: ModuleWithProviders
= RouterModule.forRoot(routes);
Here is the Plunker!!
Hope this helps!!
I am trying to implement alanning Meteor-roles with react-router in my Meteor application. Everything is working fine except the fact I can't manage properly to restrict a route using alanning roles or Meteor.user()
I tried with meteor-roles:
I am trying to use the onEnter={requireVerified} on my route. This is the code:
const requireVerified = (nextState, replace) => {
if (!Roles.userIsInRole(Meteor.userId(), ['verified'],'user_default')) {
replace({
pathname: '/account/verify',
state: { nextPathname: nextState.location.pathname },
});
}
};
I tried with Meteor.user():
const requireVerified = (nextState, replace) => {
if (!Meteor.user().isverified == true) {
replace({
pathname: '/account/verify',
state: { nextPathname: nextState.location.pathname },
});
}
};
So this is working when I am clicking on a route link, but when i manually refresh (F5), it does not work. After digging into it, i have found that Meteor.user() is not ready when i manually refresh the page.
I know Meteor.userid() or Meteor.logginIn() are working, but i wanted
to verify not just that they are logged but if they are "verified" or
have a role.
I also tried to check inside the component with react, with componentDidMount() or componentWillMount(), in both cases it's the same, the manual fresh does not load Meteor.user() before the compenent is mounted.
So what is the best way to restrict components/routes with meteor/alaning roles + react router ? (I am using react-komposer inside TheMeteorChef's base)
Thank you.
Note I have not tried it yet, it's only a suggestion
One thing you could try is to use componentWillReceiveProps alongside createContainer from 'react-meteor-data' like that:
import React, { Component, PropTypes } from 'react';
import { Meteor } from 'meteor/meteor';
import { createContainer } from 'meteor/react-meteor-data';
import { Roles } from 'meteor/alanning:roles';
class MyComponent extends Component {
componentWillReceiveProps(nextProps) {
const { user } = nextProps;
if (user && !Roles.userIsInRole(user._id, ['verified'], 'user_default')) {
browserHistory.push('/account/verify');
}
// If Meteor.user() is not ready, this will be skipped.
}
}
MyComponent.propTypes = {
user: PropTypes.object,
};
export default createContainer(() => {
const user = Meteor.user() || null;
return { user };
}, MyComponent);
To explain the flow, when the page is loaded, as you said Meteor.user() is not defined so you can't check the permissions. However, when Meteor.user() gets defined, this will trigger a refresh of the template, and the new props will be passed to componentWillReceiveProps. At this moment you can check if user has been defined and redirect if needed.
To be really sure not to miss anything, I would actually put the verification in the constructor() as well (defining a function that takes the props as arguments and calling it in both constructor() and componentWillReceiveProps()).
I have the following simplified Component for a dashboard. The dashboard object is injected via props. The handleDeleteDashboard action checks if the dashboard isn't the last available one. If it is, you are not allowed to delete it. For this check I need nrOfDashboards which I get from the store in mapStateToProps. So I connected the Component to the redux store.
class Dashboard extends Component {
constructor(props) {
super(props);
this.handleDeleteDashboard = this.handleDeleteDashboard.bind(this);
}
handleDeleteDashboard() {
const { dashboardDeleteAction, dashboard, nrOfDashboards } = this.props;
if (nrOfDashboards < 2) {
// NOT Allowed to delete
} else {
dashboardDeleteAction(dashboard.id);
}
}
render() {
const { dashboard } = this.props;
return (
<Content>
<h1>{dashboard.name}</h1>
<Button onButtonClick={this.handleDeleteDashboard}>Delete</Button>
</Content>
);
}
}
Dashboard.propTypes = {
dashboard: customPropTypes.dashboard.isRequired,
nrOfDashboards: PropTypes.number.isRequired
};
function mapStateToProps(state) {
return {
nrOfDashboards: selectNrOfDashboards(state)
}
}
export default connect(mapStateToProps, { dashboardDeleteAction: dashboardActionCreators.dashboardDelete })(Dashboard);
But now the Component is subscribed to the store and updates whenever nrOfDashboards changes (I know I can perform a shouldComponentUpdate here to prevent if from re-rendering but that is not the point). So I am basically subscribing to changes on nrOfDashboards although I only need this information when I actively click on the delete button.
So I came up with a alternative solution where I disconnected the Component from the store and access the store via context in the handleDeleteDashboard method.
class Dashboard extends Component {
constructor(props) {
...
}
handleDeleteDashboard() {
const { dashboardDeleteAction, dashboard } = this.props;
const store = this.context;
if (selectNrOfDashboards(store.getState()) < 2) {
// NOT Allowed to delete
} else {
dashboardDeleteAction(dashboard.id);
}
}
render() {
...
}
}
Dashboard.propTypes = {
dashboard: customPropTypes.dashboard.isRequired,
};
Dashboard.contextTypes = {
store: PropTypes.object
};
export default connect(null, { dashboardDeleteAction: dashboardActionCreators.dashboardDelete })(Dashboard);
This works fine for me and whenever I actively click the button I ensure to get the fresh state from the store. Anyhow, I have not seen this technique somewhere else before and also read somewhere that accessing the store should not be done outside of mapStateToProps. But my question is if direct access to the store on demand is an anti-pattern and if I better should follow code example one, where I connect the Component to the store?
Yes. Direct access to the store is considered an anti-pattern. Idiomatic Redux code uses basic dependency injection - connect() and its mapState() and mapDispatch() arguments give you the data your component needs and the reference to dispatch, and middleware like Redux-Thunk gives you access to getState() and dispatch() in your action creators.
Ideally, your component would simply dispatch an action creator, and let the action creator logic worry about whether or not to really dispatch a real action. So, in your case, that might look like:
// action creator
export function deleteDashboard(dashboardID) {
return (dispatch, getState) => {
const state = getState();
const numberOfDashboards = selectNumberOfDashboards(state);
if(numberOfDashboards >= 2) {
dispatch({
type : "DELETE_DASHBOARD",
payload : {
dashboardID
}
});
}
}
}
// component
handleDeleteDashboard() {
const {dashboard} = this.props;
this.props.dispatch(deleteDashboard(dashboard.id));
}
See the Redux FAQ question on this topic: http://redux.js.org/docs/FAQ.html#store-setup-multiple-stores
So, I see on an error, redux-promise hands me back error: true, along with the payload, but that is once it hits the reducer... to me, decoupling the request AND error condition is a bit odd, and seems inappropriate. What is an effective way to also deal with error condition when using axios w/ reduc-promise (middleware).. here is the gist of what i have..
in action/
const request = axios(SOME_URL);
return {
type: GET_ME_STUFF,
payload: request
}
in reducer/
const startState = {
whatever: [],
error: false
}
case GET_ME_STUFF:
return {...state, startState, {stuff:action.payload.data, error: action.error? true : false}}
etc... then I can deal with the error.. so, my api call is now split into two seperate areas and that seems wrong.... there must be something I am missing here. I would think in the /actions I can pass in a callback that handles a new action etc.. or something, but not split it.
I've had to go through a similar situation. The challenge is that you likely won't be able to evaluate the results of the promise until it is at the reducer. You could handle your exceptions there but it's not the best pattern. From what I've read reducers are meant only to return appropriate pieces of state based on action.type and do nothing else.
So, enter an additional middleware, redux-thunk. Instead of returning an object, it returns a function, and it can coexist with promise.
It's explained quite well at http://danmaz74.me/2015/08/19/from-flux-to-redux-async-actions-the-easy-way/ [archived here]. Essentially, you can evaluate the promise here and dispatch through the other action creators before the promise result hits the reducers.
In your actions file, add additional action creators that would handle the success and error (and any other) states.
function getStuffSuccess(response) {
return {
type: GET_ME_STUFF_SUCCESS,
payload: response
}
}
function getStuffError(err) {
return {
type: GET_ME_STUFF_ERROR,
payload: err
}
}
export function getStuff() {
return function(dispatch) {
axios.get(SOME_URL)
.then((response) => {
dispatch(getStuffSuccess(response))
})
.catch((err) => {
dispatch(getStuffError(err))
})
}
}
return null
This is roughly to how you might translate your pseudocode to what is explained at the link. This handles evaluating the promise directly in your action creator and firing off the appropriate actions and payloads to your reducers which follows the convention of action -> reducer -> state -> component update cycle. I'm still pretty new to React/Redux myself but I hope this helps.
The accepted answer doesn't make use of redux-promise. Since the question is actually about handling errors using redux-promise I provide another answer.
In the reducer you should inspect the existence of the error attribute on the action object:
// This is the reducer
export default function(previousState = null, action) {
if (action.error) {
action.type = 'HANDLE_XHR_ERROR'; // change the type
}
switch(action.type) {
...
And change the type of the action, triggering a state change for an error handling component that you have set up for this.
You can read a bit more about it here on github.
It looks like you can catch the error where you make the dispatch, then make an separate error dispatch if it happens. It's a bit of a hack but it works.
store.dispatch (function (dispatch) {
dispatch ({
type:'FOO',
payload:axios.get(url)
})
.catch (function(err) {
dispatch ({
type:"FOO" + "_REJECTED",
payload:err
});
});
});
and in the reducer
const reducer = (state=initialState, action) => {
switch (action.type) {
case "FOO_PENDING": {
return {...state, fetching: true};
}
case "FOO_REJECTED": {
return {...state, fetching: false, error: action.payload};
}
case "FOO_FULFILLED": {
return {
...state,
fetching: false,
fetched: true,
data: action.payload,
};
}
}
return state;
};
Still using redux-promises you can do something like this which I think is an elegant way to deal with this problem.
First, set a property in the redux state that will hold any ajax errors that may occurred.
ajaxError: {},
Second, setup a reducer to handle ajax errors:
export default function ajaxErrorsReducer(state = initialState.ajaxError, action) {
if (action.error) {
const { response } = action.payload;
return {
status: response.status,
statusText: response.statusText,
message: response.data.message,
stack: response.data.stack,
};
}
return state;
}
Finally, create a very simple react component that will render errors if there are any (I am using the react-s-alert library to show nice alerts):
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import Alert from 'react-s-alert';
class AjaxErrorsHandler extends Component {
constructor(props, context) {
super(props, context);
this.STATUS_GATE_WAY_TIMEOUT = 504;
this.STATUS_SERVICE_UNAVAILABLE = 503;
}
componentWillReceiveProps(nextProps) {
if (this.props.ajaxError !== nextProps.ajaxError) {
this.showErrors(nextProps.ajaxError);
}
}
showErrors(ajaxError) {
if (!ajaxError.status) {
return;
}
Alert.error(this.getErrorComponent(ajaxError), {
position: 'top-right',
effect: 'jelly',
timeout: 'none',
});
}
getErrorComponent(ajaxError) {
let customMessage;
if (
ajaxError.status === this.STATUS_GATE_WAY_TIMEOUT ||
ajaxError.status === this.STATUS_SERVICE_UNAVAILABLE
) {
customMessage = 'The server is unavailable. It will be restored very shortly';
}
return (
<div>
<h3>{ajaxError.statusText}</h3>
<h5>{customMessage ? customMessage : ajaxError.message}</h5>
</div>
);
}
render() {
return (
<div />
);
}
}
AjaxErrorsHandler.defaultProps = {
ajaxError: {},
};
AjaxErrorsHandler.propTypes = {
ajaxError: PropTypes.object.isRequired,
};
function mapStateToProps(reduxState) {
return {
ajaxError: reduxState.ajaxError,
};
}
export default connect(mapStateToProps, null)(AjaxErrorsHandler);
You can include this component in your App component.
This might not be the best approach but it works for me. I pass the 'this' of my component as var context. Then when i get response back i just execute the methods defined in my components context. In my component i have successHdl and errorHdl. From there i can trigger more redux actions as normal. I checked all the previous answers and seem too daunting for such a trivial task.
export function updateJob(payload, context){
const request = axios.put(UPDATE_SOMETHING, payload).then(function (response) {
context.successHdl(response);
})
.catch(function (error) {
context.errorHdl(error);
});;
return {
type: UPDATE_SOMETHING,
payload: payload,
}
}
Don't use redux-promise. It overcomplicates something that's actually super simple to do yourself.
Instead read the redux docs: http://redux.js.org/docs/advanced/AsyncActions.html
It'll give you a much better understanding of how to handle this kind of interactions and you'll learn how to write something (better than) redux-promise yourself.