Closed
Description
I'm trying to make a login success/error flow but my main concern is where I can put this logic.
Currently I'm using actions -> reducer (switch case with action calling API) -> success/error on response triggering another action
.
The problem with this approach is that the reducer is not working when I call the action from the API call.
am I missing something?
Reducer
import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';
import Immutable from 'immutable';
import LoginApiCall from '../utils/login-request';
const initialState = new Immutable.Map({
email: '',
password: '',
}).asMutable();
export default function user(state = initialState, action) {
switch (action.type) {
case LOGIN_ATTEMPT:
console.log(action.user);
LoginApiCall.login(action.user);
return state;
case LOGGED_FAILED:
console.log('failed from reducer');
return state;
case LOGGED_SUCCESSFULLY:
console.log('success', action);
console.log('success from reducer');
break;
default:
return state;
}
}
actions
import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';
export function loginError(error) {
return dispatch => {
dispatch({ error, type: LOGGED_FAILED });
};
}
/*
* Should add the route like parameter in this method
*/
export function loginSuccess(response) {
return dispatch => {
dispatch({ response, type: LOGGED_SUCCESSFULLY });
// router.transitionTo('/dashboard'); // will fire CHANGE_ROUTE in its change handler
};
}
export function loginRequest(email, password) {
const user = {email: email, password: password};
return dispatch => {
dispatch({ user, type: LOGIN_ATTEMPT });
};
}
API calls
// Use there fetch polyfill
// The main idea is create a helper in order to handle success/error status
import * as LoginActions from '../actions/LoginActions';
const LoginApiCall = {
login(userData) {
fetch('http://localhost/login', {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: userData.email,
password: userData.password,
}),
})
.then(response => {
if (response.status >= 200 && response.status < 300) {
console.log(response);
LoginActions.loginSuccess(response);
} else {
const error = new Error(response.statusText);
error.response = response;
LoginActions.loginError();
throw error;
}
})
.catch(error => { console.log('request failed', error); });
},
};
export default LoginApiCall;
Activity
gaearon commentedon Jul 20, 2015
This is almost correct, but the problem is you can't just call pure action creators and expect things to happen. Don't forget your action creators are just functions that specify what needs to be dispatched.
gaearon commentedon Jul 20, 2015
Now, let's get to your example. The first thing I want to clarify is you don't need async
dispatch => {}
form if you only dispatch a single action synchronously and don't have side effects (true forloginError
andloginRequest
).This:
can be simplified as
gaearon commentedon Jul 20, 2015
Secondly, your reducers are supposed to be pure functions that don't have side effects. Do not attempt to call your API from a reducer. Reducer is synchronous and passive, it only modifies the state.
This:
should probably look more like
gaearon commentedon Jul 20, 2015
Finally, where do you put the login API call then?
This is precisely what
dispatch => {}
action creator is for. Side effects!It's just another action creator. Put it together with other actions:
In your components, just call
gaearon commentedon Jul 20, 2015
Finally, if you find yourself often writing large action creators like this, it's a good idea to write a custom middleware for async calls compatible with promises are whatever you use for async.
The technique I described above (action creators with
dispatch => {}
signature) is right now included in Redux, but in 1.0 will be available only as a separate package called redux-thunk. While you're at it, you might as well check out redux-promise-middleware or redux-promise.dariocravero commentedon Jul 20, 2015
@gaearon 👏 that was an amazing explanation! ;) It should definitely make it into the docs.
andreychev commentedon Jul 20, 2015
@gaearon awesome explanation! 🏆
tomkis commentedon Jul 20, 2015
@gaearon What if you need to perform some logic to determine which API call should be called? Isn't it domain logic which should be in one place(reducers)? Keeping that in Action creators sounds to me like breaking single source of truth. Besides, mostly you need to parametrise the API call by some value from the application state. You most likely also want to test the logic somehow. We found making API calls in Reducers (atomic flux) very helpful and testable in large scale project.
gaearon commentedon Jul 20, 2015
This breaks record/replay. Functions with side effects are harder to test than pure functions by definition. You can use Redux like this but it's completely against its design. :-)
“Single source of truth” means data lives in one place, and has no independent copies. It doesn't mean “all domain logic should be in one place”.
Reducers specify how state is transformed by actions. They shouldn't worry about where these actions originate. They may come from components, action creators, recorded serialized session, etc. This is the beauty of the concept of using actions.
Any API calls (or logic that determines which API is called) happens before reducers. This is why Redux supports middleware. Thunk middleware described above lets you use conditionals and even read from the state:
Action creators and middleware are designed to perform side effects and complement each other.
Reducers are just state machines and have nothing to do with async.
tomkis commentedon Jul 20, 2015
This is indeed very valid point, thanks.
gaearon commentedon Jul 20, 2015
I'll reopen for posterity until a version of this is in the docs.
104 remaining items
gaearon commentedon Jan 8, 2016
No, this wasn't my point. Take a look at
makeSandwichesForEverybody
. It's a thunk action creator calling other thunk action creators. This is why you don't need to put everything in components. See alsoasync
example in this repo for more of this.catamphetamine commentedon Jan 8, 2016
@gaearon But I think it wouldn't be appropriate to put, say, my fancy animation code into the action creator, would it?
Consider this example:
How would you rewrite it the Right way?
slorber commentedon Jan 8, 2016
@halt-hammerzeit you can make the actionCreator return the promises so that the component can show some spinner or whatever by using local component state (hard to avoid when using jquery anyway)
Otherwise you can manage complex timers to drive animations with redux-saga.
Take a look at this blog post: http://jaysoo.ca/2016/01/03/managing-processes-in-redux-using-sagas/
gaearon commentedon Jan 8, 2016
Oh, in this case it's fine to keep it in component.
(Note: generally in React it's best to use declarative model for everything rather than showing modals imperatively with jQuery. But no big deal.)
catamphetamine commentedon Jan 8, 2016
@slorber Yeah, I'm already returning Promises and stuff from my "thunks" (or whatever you call them; errr, i don't like this weird word), so I can handle spinners and such.
@gaearon Ok, I got you. In the ideal machinery world it would of course be possible to stay inside the declarative programming model, but reality has its own demands, irrational, say, from the view of a machine. People are irrational beings not just operating with pure zeros and ones and it requires compromising the code beauty and purity in favour of being able to do some irrational stuff.
I'm satisfied with the support in this thread. My doubts seem to be resolved now.
gaearon commentedon Mar 18, 2016
Relevant new discussion: #1528
juliandavidmr commentedon Aug 27, 2016
@gaearon very awesome explanation! 🏆 Thanks 👍