New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trying to put API calls in the correct place #291

Closed
5h1rU opened this Issue Jul 20, 2015 · 115 comments

Comments

@5h1rU

5h1rU commented Jul 20, 2015

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;
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

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.

// CounterActions
export function increment() {
  return { type: INCREMENT }
}


// Some other file
import { increment } from './CounterActions';
store.dispatch(increment()); <--- This will work assuming you have a reference to the Store
increment(); <---- THIS DOESN'T DO ANYTHING! You're just calling your function and ignoring result.


// SomeComponent
import { increment } from './CounterActions';

@connect(state => state.counter) // will inject store's dispatch into props
class SomeComponent {
  render() {
    return <OtherComponent {...bindActionCreators(CounterActions, this.props.dispatch)} />
  }
}


// OtherComponent
class OtherComponent {
  handleClick() {
    // this is correct:
    this.props.increment(); // <---- it was bound to dispatch in SomeComponent

    // THIS DOESN'T DO ANYTHING:
    CounterActions.increment(); // <---- it's just your functions as is! it's not bound to the Store.
  }
}
Collaborator

gaearon commented 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.

// CounterActions
export function increment() {
  return { type: INCREMENT }
}


// Some other file
import { increment } from './CounterActions';
store.dispatch(increment()); <--- This will work assuming you have a reference to the Store
increment(); <---- THIS DOESN'T DO ANYTHING! You're just calling your function and ignoring result.


// SomeComponent
import { increment } from './CounterActions';

@connect(state => state.counter) // will inject store's dispatch into props
class SomeComponent {
  render() {
    return <OtherComponent {...bindActionCreators(CounterActions, this.props.dispatch)} />
  }
}


// OtherComponent
class OtherComponent {
  handleClick() {
    // this is correct:
    this.props.increment(); // <---- it was bound to dispatch in SomeComponent

    // THIS DOESN'T DO ANYTHING:
    CounterActions.increment(); // <---- it's just your functions as is! it's not bound to the Store.
  }
}
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

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 for loginError and loginRequest).

This:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

can be simplified as

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

// You'll have a side effect here so (dispatch) => {} form is a good idea
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}
Collaborator

gaearon commented 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 for loginError and loginRequest).

This:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return dispatch => {
    dispatch({ error, type: LOGGED_FAILED });
  };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return dispatch => {
    dispatch({ user, type: LOGIN_ATTEMPT });
  };
}

can be simplified as

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

// You'll have a side effect here so (dispatch) => {} form is a good idea
export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    // router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

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:

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
}).asMutable(); // <---------------------- why asMutable?

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user); // <------------------------ no side effects in reducers! :-(
      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;
  }

should probably look more like

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
});

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return state.merge({
        isLoggingIn: true,
        isLoggedIn: false,
        email: action.email,
        password: action.password // Note you shouldn't store user's password in real apps
      });
    case LOGGED_FAILED:
      return state.merge({
        error: action.error,
        isLoggingIn: false,
        isLoggedIn: false
      });
    case LOGGED_SUCCESSFULLY:
      return state.merge({
        error: null,
        isLoggingIn: false,
        isLoggedIn: true
      });
      break;
    default:
      return state;
  }
Collaborator

gaearon commented 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:

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
}).asMutable(); // <---------------------- why asMutable?

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      console.log(action.user);
      LoginApiCall.login(action.user); // <------------------------ no side effects in reducers! :-(
      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;
  }

should probably look more like

const initialState = new Immutable.Map({
  email: '',
  password: '',
  isLoggingIn: false,
  isLoggedIn: false,
  error: null
});

export default function user(state = initialState, action) {
  switch (action.type) {
    case LOGIN_ATTEMPT:
      return state.merge({
        isLoggingIn: true,
        isLoggedIn: false,
        email: action.email,
        password: action.password // Note you shouldn't store user's password in real apps
      });
    case LOGGED_FAILED:
      return state.merge({
        error: action.error,
        isLoggingIn: false,
        isLoggedIn: false
      });
    case LOGGED_SUCCESSFULLY:
      return state.merge({
        error: null,
        isLoggingIn: false,
        isLoggedIn: true
      });
      break;
    default:
      return state;
  }
@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

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:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    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);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

In your components, just call

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector
Collaborator

gaearon commented 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:

import { LOGIN_ATTEMPT, LOGGED_FAILED, LOGGED_SUCCESSFULLY } from '../constants/LoginActionTypes';

export function loginError(error) {
  return { error, type: LOGGED_FAILED };
}

export function loginSuccess(response) {
  return dispatch => {
    dispatch({ response, type: LOGGED_SUCCESSFULLY });
    router.transitionTo('/dashboard');
  };
}

export function loginRequest(email, password) {
  const user = {email: email, password: password};
  return { user, type: LOGIN_ATTEMPT };
}

export function login(userData) {
  return dispatch =>
    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);
        dispatch(loginSuccess(response));
      } else {
        const error = new Error(response.statusText);
        error.response = response;
        dispatch(loginError(error));
        throw error;
      }
    })
    .catch(error => { console.log('request failed', error); });
}

In your components, just call

this.props.login(); // assuming it was bound with bindActionCreators before

// --or--

this.props.dispatch(login()); // assuming you only have dispatch from Connector

@gaearon gaearon closed this Jul 20, 2015

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

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.

Collaborator

gaearon commented 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.

@acdlite acdlite referenced this issue Jul 20, 2015

Closed

Docs improvements #140

6 of 13 tasks complete
@dariocravero

This comment has been minimized.

Show comment
Hide comment
@dariocravero

dariocravero Jul 20, 2015

Contributor

@gaearon 👏 that was an amazing explanation! ;) It should definitely make it into the docs.

Contributor

dariocravero commented Jul 20, 2015

@gaearon 👏 that was an amazing explanation! ;) It should definitely make it into the docs.

@andreychev

This comment has been minimized.

Show comment
Hide comment
@andreychev

andreychev Jul 20, 2015

@gaearon awesome explanation! 🏆

andreychev commented Jul 20, 2015

@gaearon awesome explanation! 🏆

@tomkis

This comment has been minimized.

Show comment
Hide comment
@tomkis

tomkis Jul 20, 2015

Contributor

@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.

Contributor

tomkis commented 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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

We found making API calls in Reducers (atomic flux) very helpful and testable in large scale project.

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. :-)

Keeping that in Action creators sounds to me like breaking single source of truth.

“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”.

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)?

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:

// Simple pure action creator
function loginFailure(error) {
  return { type: LOGIN_FAILURE, error };
}

// Another simple pure action creator
function loginSuccess(userId) {
  return { type: LOGIN_SUCCESS, userId };
}

// Another simple pure action creator
function logout() {
  return { type: LOGOUT };  
}


// Side effect: uses thunk middleware
function login() {
  return dispatch => {
    MyAwesomeAPI.performLogin().then(
      json => dispatch(loginSuccess(json.userId)),
      error => dispatch(loginFailure(error))
    )
  };
}


// Side effect *and* reads state
function toggleLoginState() {
  return (dispatch, getState) => {
    const { isLoggedIn } = getState().loginState;
    if (isLoggedIn) {
      dispatch(login());
    } else {
      dispatch(logout());
    }
  };
}

// Component
this.props.toggleLoginState(); // Doesn't care how it happens

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.

Collaborator

gaearon commented Jul 20, 2015

We found making API calls in Reducers (atomic flux) very helpful and testable in large scale project.

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. :-)

Keeping that in Action creators sounds to me like breaking single source of truth.

“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”.

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)?

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:

// Simple pure action creator
function loginFailure(error) {
  return { type: LOGIN_FAILURE, error };
}

// Another simple pure action creator
function loginSuccess(userId) {
  return { type: LOGIN_SUCCESS, userId };
}

// Another simple pure action creator
function logout() {
  return { type: LOGOUT };  
}


// Side effect: uses thunk middleware
function login() {
  return dispatch => {
    MyAwesomeAPI.performLogin().then(
      json => dispatch(loginSuccess(json.userId)),
      error => dispatch(loginFailure(error))
    )
  };
}


// Side effect *and* reads state
function toggleLoginState() {
  return (dispatch, getState) => {
    const { isLoggedIn } = getState().loginState;
    if (isLoggedIn) {
      dispatch(login());
    } else {
      dispatch(logout());
    }
  };
}

// Component
this.props.toggleLoginState(); // Doesn't care how it happens

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

This comment has been minimized.

Show comment
Hide comment
@tomkis

tomkis Jul 20, 2015

Contributor

Reducers specify how state is transformed by actions.

This is indeed very valid point, thanks.

Contributor

tomkis commented Jul 20, 2015

Reducers specify how state is transformed by actions.

This is indeed very valid point, thanks.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 20, 2015

Collaborator

I'll reopen for posterity until a version of this is in the docs.

Collaborator

gaearon commented Jul 20, 2015

I'll reopen for posterity until a version of this is in the docs.

@suhaotian

This comment has been minimized.

Show comment
Hide comment
@suhaotian

suhaotian commented Jul 21, 2015

👍

@vladar

This comment has been minimized.

Show comment
Hide comment
@vladar

vladar Jul 23, 2015

I would argue that while reducers are state machines, so are action creators in general (but implicitly).

Say user clicked "submit" button twice. First time you will send HTTP request to the server (in the action creator) and change state to "submitting" (in the reducer). Second time you do not whant to do that.

In current model your action creator will have to choose what actions to dispatch depending on current state. If it is not currently "submitting" - then send HTTP request and dispatch one action, otherwise - just do nothing or even dispatch different action (like warning).

So your control flow is effectively split into two state machines and one of them is implicit and full of side effects.

I'd say that this Flux approach handles the majority of use-cases of typical web app very well. But when you have complex control logic it may become problematic. All of existing examples just do not have complex control logic, so this problem is somewhat hidden.

https://github.com/Day8/re-frame is an example of different approach where they have single event handler which acts as the only FSM.

But then reducers have side effects. So it is interesting how they deal with "replay" feature in such cases - asked them in Day8/re-frame#86

In general, I think this is a real problem and it's not a surprise that it appears again and again. It's interesting to see what solutions will emerge eventually.

vladar commented Jul 23, 2015

I would argue that while reducers are state machines, so are action creators in general (but implicitly).

Say user clicked "submit" button twice. First time you will send HTTP request to the server (in the action creator) and change state to "submitting" (in the reducer). Second time you do not whant to do that.

In current model your action creator will have to choose what actions to dispatch depending on current state. If it is not currently "submitting" - then send HTTP request and dispatch one action, otherwise - just do nothing or even dispatch different action (like warning).

So your control flow is effectively split into two state machines and one of them is implicit and full of side effects.

I'd say that this Flux approach handles the majority of use-cases of typical web app very well. But when you have complex control logic it may become problematic. All of existing examples just do not have complex control logic, so this problem is somewhat hidden.

https://github.com/Day8/re-frame is an example of different approach where they have single event handler which acts as the only FSM.

But then reducers have side effects. So it is interesting how they deal with "replay" feature in such cases - asked them in Day8/re-frame#86

In general, I think this is a real problem and it's not a surprise that it appears again and again. It's interesting to see what solutions will emerge eventually.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 23, 2015

Collaborator

Keep us posted about re-frame and side effects!

Collaborator

gaearon commented Jul 23, 2015

Keep us posted about re-frame and side effects!

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 23, 2015

In my book there are 3 state types in any decent Redux web app.

  1. View components (React this.state). This should be avoided but sometimes I just want to implement some behavior which can be reused by just reusing a component independently of Redux.

  2. App shared state, i. e. Redux.state. It is state a view layer is using to present an app to a user and view components use it to "communicate" between each other. And this state can be used to "communicate" between ActionCreators, Middlewares, views as they decisions can depend on this state. Hence the "shared" is important. It doesn't have to be a complete app state though. Or I think it is impractical to implement everything as this type of state (in terms of actions).

  3. State which is held by other side-effecting complex modules/libs/services. I would write these to handle scenarios vladar is describing. Or react-router is another example. You can treat it as a black-box which has its own state or you can decide to lift part of react-router state into app shared state (Redux.state). If I would write a complex HTTP module which would cleverly handle all my requests and its timings, I'm usually not interested in request timing details in Redux.state. I would only use this module from ActionCreators/Middlewares, possibly together with Redux.state to get new Redux.state.

If I would like to write view component showing my request's timings I would have to get this state to Redux.state.

@vladar Is it what you mean that AC/MW are implicit state machines? That because they don't held state by itself but they are still dependent on state held somewhere else and that they can define control logic how this states evolve in time? For some cases, I think they still could be implemented as closures and held its own state, becoming explicit state machines?

Alternatively I could call Redux.state a "public state", while other states are private. How to design my app is an act to decide what to keep as a private state and what as a public state. It seems to me like nice opportunity for encapsulation, hence I don't see it problematic to have state divided into different places as far as it doesn't become a hell how they affect each other.

vladap commented Jul 23, 2015

In my book there are 3 state types in any decent Redux web app.

  1. View components (React this.state). This should be avoided but sometimes I just want to implement some behavior which can be reused by just reusing a component independently of Redux.

  2. App shared state, i. e. Redux.state. It is state a view layer is using to present an app to a user and view components use it to "communicate" between each other. And this state can be used to "communicate" between ActionCreators, Middlewares, views as they decisions can depend on this state. Hence the "shared" is important. It doesn't have to be a complete app state though. Or I think it is impractical to implement everything as this type of state (in terms of actions).

  3. State which is held by other side-effecting complex modules/libs/services. I would write these to handle scenarios vladar is describing. Or react-router is another example. You can treat it as a black-box which has its own state or you can decide to lift part of react-router state into app shared state (Redux.state). If I would write a complex HTTP module which would cleverly handle all my requests and its timings, I'm usually not interested in request timing details in Redux.state. I would only use this module from ActionCreators/Middlewares, possibly together with Redux.state to get new Redux.state.

If I would like to write view component showing my request's timings I would have to get this state to Redux.state.

@vladar Is it what you mean that AC/MW are implicit state machines? That because they don't held state by itself but they are still dependent on state held somewhere else and that they can define control logic how this states evolve in time? For some cases, I think they still could be implemented as closures and held its own state, becoming explicit state machines?

Alternatively I could call Redux.state a "public state", while other states are private. How to design my app is an act to decide what to keep as a private state and what as a public state. It seems to me like nice opportunity for encapsulation, hence I don't see it problematic to have state divided into different places as far as it doesn't become a hell how they affect each other.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 23, 2015

@vladar

But then reducers have side effects. So it is interesting how they deal with "replay" feature in such cases

To achieve easy replay the code has to be deterministic. It is what Redux achieves by requiring pure reducers. In an effect Redux.state divides app into non-deterministic (async) and deterministic parts. You can replay bahavior above Redux.state assuming you don't do crazy thing in view components. The first important goal to achieve deterministic code is to move async code away and translate it into synchronous code via action log. It is what Flux architecture does in general. But in general it is not enough and other side-effecting or mutating code can still break deterministic processing.

Achieving replay-ability with side-effecting reducers is imho either impractically hard, even impossible or it will work only partially with many corner cases with probably a little practical effect.

vladap commented Jul 23, 2015

@vladar

But then reducers have side effects. So it is interesting how they deal with "replay" feature in such cases

To achieve easy replay the code has to be deterministic. It is what Redux achieves by requiring pure reducers. In an effect Redux.state divides app into non-deterministic (async) and deterministic parts. You can replay bahavior above Redux.state assuming you don't do crazy thing in view components. The first important goal to achieve deterministic code is to move async code away and translate it into synchronous code via action log. It is what Flux architecture does in general. But in general it is not enough and other side-effecting or mutating code can still break deterministic processing.

Achieving replay-ability with side-effecting reducers is imho either impractically hard, even impossible or it will work only partially with many corner cases with probably a little practical effect.

@tomkis

This comment has been minimized.

Show comment
Hide comment
@tomkis

tomkis Jul 23, 2015

Contributor

To achieve easy timetravel we store snapshots of app state instead of replaying actions (which is always hard) as done in Redux.

Contributor

tomkis commented Jul 23, 2015

To achieve easy timetravel we store snapshots of app state instead of replaying actions (which is always hard) as done in Redux.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 23, 2015

Collaborator

To achieve easy timetravel we store snapshots of app state instead of replaying actions (which is always hard) as done in Redux.

Redux DevTools store both snapshots and actions. If you don't have actions, you can't hot reload the reducers. It's the most powerful feature of the workflow that DevTools enable.

Time travel works fine with impure action creators because it happens on the level of the dispatched (final) raw actions. These are plain objects so they're completely deterministic. Yes, you can't “rollback” an API call, but I don't see a problem with that. You can rollback the raw actions emitted by it.

Collaborator

gaearon commented Jul 23, 2015

To achieve easy timetravel we store snapshots of app state instead of replaying actions (which is always hard) as done in Redux.

Redux DevTools store both snapshots and actions. If you don't have actions, you can't hot reload the reducers. It's the most powerful feature of the workflow that DevTools enable.

Time travel works fine with impure action creators because it happens on the level of the dispatched (final) raw actions. These are plain objects so they're completely deterministic. Yes, you can't “rollback” an API call, but I don't see a problem with that. You can rollback the raw actions emitted by it.

@vladar

This comment has been minimized.

Show comment
Hide comment
@vladar

vladar Jul 23, 2015

That because they don't held state by itself but they are still dependent on state held somewhere else and that they can define control logic how this states evolve in time?

Yeah, that's exactly what I mean. But you may also end up with duplication of your control logic. Some code to demonstrate the problem (from my example above with double submit click).

function submit(data) {
  return (dispatch, getState) => {
    const { isSubmitting } = getState().isSubmitting;
    if (!isSubmitting) {
      dispatch(started(data));

      MyAPI.submit(data).then(
        json => dispatch(success(json)),
        error => dispatch(failure(error))
      )
    }
  }
}

function started(data) {
   return { type: SUBMIT_STARTED, data };
}

function success(result) {
  return { type: SUBMIT_SUCCESS, result };
}

function failure(error) {
  return { type: SUBMIT_FAILURE, error };
}

And then you have your reducer to check "isSubmitting" state again

const initialState = new Immutable.Map({
  isSubmitting: false,
  pendingData: null,
  error: null
});

export default function form(state = initialState, action) {
  switch (action.type) {
    case SUBMIT_STARTED:
      if (!state.isSubmitting) {
        return state.merge({
          isSubmitting: true,
          pendingData: action.data
        });
      } else {
        return state;
      }
  }
}

So you end-up having same logical checks in two places. Obviously duplication in this example is minimal, but for more complex scenarious it may get not pretty.

vladar commented Jul 23, 2015

That because they don't held state by itself but they are still dependent on state held somewhere else and that they can define control logic how this states evolve in time?

Yeah, that's exactly what I mean. But you may also end up with duplication of your control logic. Some code to demonstrate the problem (from my example above with double submit click).

function submit(data) {
  return (dispatch, getState) => {
    const { isSubmitting } = getState().isSubmitting;
    if (!isSubmitting) {
      dispatch(started(data));

      MyAPI.submit(data).then(
        json => dispatch(success(json)),
        error => dispatch(failure(error))
      )
    }
  }
}

function started(data) {
   return { type: SUBMIT_STARTED, data };
}

function success(result) {
  return { type: SUBMIT_SUCCESS, result };
}

function failure(error) {
  return { type: SUBMIT_FAILURE, error };
}

And then you have your reducer to check "isSubmitting" state again

const initialState = new Immutable.Map({
  isSubmitting: false,
  pendingData: null,
  error: null
});

export default function form(state = initialState, action) {
  switch (action.type) {
    case SUBMIT_STARTED:
      if (!state.isSubmitting) {
        return state.merge({
          isSubmitting: true,
          pendingData: action.data
        });
      } else {
        return state;
      }
  }
}

So you end-up having same logical checks in two places. Obviously duplication in this example is minimal, but for more complex scenarious it may get not pretty.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jul 23, 2015

Collaborator

I think in the last example the check should not be inside the reducer. Otherwise it can quickly descend into inconsistency (e.g. action is dispatched by mistake, but ignored, but then the “success” action is also dispatched but it was unexpected, and state might get merged incorrectly).

(Sorry if that was the point you were making ;-)

Collaborator

gaearon commented Jul 23, 2015

I think in the last example the check should not be inside the reducer. Otherwise it can quickly descend into inconsistency (e.g. action is dispatched by mistake, but ignored, but then the “success” action is also dispatched but it was unexpected, and state might get merged incorrectly).

(Sorry if that was the point you were making ;-)

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 23, 2015

I agree with gaeron because the !isSubmitting is already encoded in the fact that SUBMIT_STARTED action has been dispatched. When an action is a result of some logic than that logic should not be replicated in reducer. Then reducer responsibility is just that when anytime it receives SUBMIT_STARTED it doesn't think and just updates state with action payload because somebody else had taken the responsibility to decide that SUBMIT_STARTED.

One should always aim that there is a single thing which takes responsibility for a single decision.

Reducer can then further build on SUBMIT_STARTED fact and extend it with additional logic but this logic should be different. F.e.:

case SUBMIT_STARTED:
  if (goodMood) loading...
  else nothing_happens

I can imagine that it can be sometimes tricky to decide who should be responsible for what. ActionCreator, Middleware, standalone complex module, reducer?

I would aim to have as much decisions as possible in reducers while keeping them pure, because they can be hot-reloaded. If decisions are required for side-effects/async then they go to AC/MW/somewhere_else. It can as well depend how best practices evolve regarding code-reuse, i.e. reducers vs. middleware.

vladap commented Jul 23, 2015

I agree with gaeron because the !isSubmitting is already encoded in the fact that SUBMIT_STARTED action has been dispatched. When an action is a result of some logic than that logic should not be replicated in reducer. Then reducer responsibility is just that when anytime it receives SUBMIT_STARTED it doesn't think and just updates state with action payload because somebody else had taken the responsibility to decide that SUBMIT_STARTED.

One should always aim that there is a single thing which takes responsibility for a single decision.

Reducer can then further build on SUBMIT_STARTED fact and extend it with additional logic but this logic should be different. F.e.:

case SUBMIT_STARTED:
  if (goodMood) loading...
  else nothing_happens

I can imagine that it can be sometimes tricky to decide who should be responsible for what. ActionCreator, Middleware, standalone complex module, reducer?

I would aim to have as much decisions as possible in reducers while keeping them pure, because they can be hot-reloaded. If decisions are required for side-effects/async then they go to AC/MW/somewhere_else. It can as well depend how best practices evolve regarding code-reuse, i.e. reducers vs. middleware.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 24, 2015

AC can't mutate state.isSubmitting directly. Hence, if its logic depends on it AC needs guarantees that state.isSubmitting will be in sync with its logic. If AC logic wants to set state.isSubmitted to true with new data and reads is back it expects it is set as such. There shouldn't by other logic potentially changing this premise. ACTION is the sync primitive itself. Basically actions define protocol and protocols should be well and clearly defined.

But I think I know what you are trying to say. Redux.state is shared state. Shared state is always tricky. It is easy to write code in reducers which changes state in a way which is unexpected by AC logic. Hence I can imagine it can be hard to keep logic between AC and reducers in sync in some very complex scenarios. This can lead to bugs which might be hard to follow and debug. I believe that for such scenarios it is always possible to collapse the logic to AC/Middleware and make reducers only stupidly act based on well defined actions with no or little logic.

vladap commented Jul 24, 2015

AC can't mutate state.isSubmitting directly. Hence, if its logic depends on it AC needs guarantees that state.isSubmitting will be in sync with its logic. If AC logic wants to set state.isSubmitted to true with new data and reads is back it expects it is set as such. There shouldn't by other logic potentially changing this premise. ACTION is the sync primitive itself. Basically actions define protocol and protocols should be well and clearly defined.

But I think I know what you are trying to say. Redux.state is shared state. Shared state is always tricky. It is easy to write code in reducers which changes state in a way which is unexpected by AC logic. Hence I can imagine it can be hard to keep logic between AC and reducers in sync in some very complex scenarios. This can lead to bugs which might be hard to follow and debug. I believe that for such scenarios it is always possible to collapse the logic to AC/Middleware and make reducers only stupidly act based on well defined actions with no or little logic.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 24, 2015

Somebody can always add new reducer which will break some old dependent AC. One should think twice before making AC dependent on Redux.state.

vladap commented Jul 24, 2015

Somebody can always add new reducer which will break some old dependent AC. One should think twice before making AC dependent on Redux.state.

@merk

This comment has been minimized.

Show comment
Hide comment
@merk

merk Jul 24, 2015

Contributor

I have an action creator that requires an authentication token returned by an API request with user credentials. There is a time limit on this token and it needs to be refreshed and managed by something. What you're proposing seems to indicate that this state doesnt belong in Redux.state?

Where should it go? Conceptually does it get passed into the constructor of the Redux store as immutable outer-state?

Contributor

merk commented Jul 24, 2015

I have an action creator that requires an authentication token returned by an API request with user credentials. There is a time limit on this token and it needs to be refreshed and managed by something. What you're proposing seems to indicate that this state doesnt belong in Redux.state?

Where should it go? Conceptually does it get passed into the constructor of the Redux store as immutable outer-state?

@vladar

This comment has been minimized.

Show comment
Hide comment
@vladar

vladar Jul 24, 2015

Hence I can imagine it can be hard to keep logic between AC and reducers in sync in some very complex scenarios.

Yep. I am not saying that it's a show-stopper, but it may become inconsistent. The point of state machine is to react to events - regardless of order of their appearence - and stay consistent.

If you rely on proper ordering of events from AC - you just implicitly move parts of your state engine to AC and couple it with reducer.

Maybe it's just a matter of preference, but I feel much better when store / reducer always acts consistently and doesn't rely on something external to keep consistency somewhere else.

One should think twice before making AC dependent on Redux.state.

I think you can't avoid it in complex cases (without moving some side effects to reducers/event-handlers).

Looks like it is a tradeoff: "dropping hot-reloading or replay feature" vs "managing state in one place". In later case you can write real FSMs or Statecharts like re-frame proposes, in former case it's more ad-hoc control logic as we have in Flux now.

vladar commented Jul 24, 2015

Hence I can imagine it can be hard to keep logic between AC and reducers in sync in some very complex scenarios.

Yep. I am not saying that it's a show-stopper, but it may become inconsistent. The point of state machine is to react to events - regardless of order of their appearence - and stay consistent.

If you rely on proper ordering of events from AC - you just implicitly move parts of your state engine to AC and couple it with reducer.

Maybe it's just a matter of preference, but I feel much better when store / reducer always acts consistently and doesn't rely on something external to keep consistency somewhere else.

One should think twice before making AC dependent on Redux.state.

I think you can't avoid it in complex cases (without moving some side effects to reducers/event-handlers).

Looks like it is a tradeoff: "dropping hot-reloading or replay feature" vs "managing state in one place". In later case you can write real FSMs or Statecharts like re-frame proposes, in former case it's more ad-hoc control logic as we have in Flux now.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 25, 2015

I have read (too quickly) through Re-frame and it is the same sort of thing as Redux with some implementation details differences. Re-frame view components are based on observables, Events are Actions, Re-frame dispatches events (actions) directly as objects (they are constructed in a view component) without using creators, Event Handlers are pure and they are the same thing as Redux reducers.

Handling side-effects isn't discussed much or I missed that, but what I'm assuming is that they are handled in Middlewares. And there is the main difference. Middlewares wrap Event handlers (reducers), compose with them from outside, while Redux Middlewares don't wrap reducers. In other words Re-frame composes side-effects directly to pure reducers while Redux keeps them separated.

It means that one can't record Re-frame events and easily replay them.

When I was talking about possible inconsistencies, I considered shared state as the root cause and I believe Re-frame has the same risk.

The difference is in coupling Middlewares with reducers. When Middlewares finish in Re-frame they directly pass result to event handlers (reducers), this result is not recorded as event/action, hence not replayable. I would say Redux just puts hooks in between instead in form of actions, hence I believe I can technically achieve the same what Re-frame does, with more flexibility at the expense of more typing (creating actions) and probably more room to do it wrong.

In the end there is nothing is Redux stopping me to implement exactly the same approach as in Re-frame. I can create any function which take reducer as a parameter, do some processing in it then directly call reducer function with the result and call it Reducer Middlewares or whatever. It is just about if I want to make it a bit more easier at the expense of loosing DevTools and more coupling.

I still have to think more if there is an significant advantage in Re-frame approach above some simplification (decreasing amount of actions). I'm open to prove me wrong, as I have said I read to quickly through it.

vladap commented Jul 25, 2015

I have read (too quickly) through Re-frame and it is the same sort of thing as Redux with some implementation details differences. Re-frame view components are based on observables, Events are Actions, Re-frame dispatches events (actions) directly as objects (they are constructed in a view component) without using creators, Event Handlers are pure and they are the same thing as Redux reducers.

Handling side-effects isn't discussed much or I missed that, but what I'm assuming is that they are handled in Middlewares. And there is the main difference. Middlewares wrap Event handlers (reducers), compose with them from outside, while Redux Middlewares don't wrap reducers. In other words Re-frame composes side-effects directly to pure reducers while Redux keeps them separated.

It means that one can't record Re-frame events and easily replay them.

When I was talking about possible inconsistencies, I considered shared state as the root cause and I believe Re-frame has the same risk.

The difference is in coupling Middlewares with reducers. When Middlewares finish in Re-frame they directly pass result to event handlers (reducers), this result is not recorded as event/action, hence not replayable. I would say Redux just puts hooks in between instead in form of actions, hence I believe I can technically achieve the same what Re-frame does, with more flexibility at the expense of more typing (creating actions) and probably more room to do it wrong.

In the end there is nothing is Redux stopping me to implement exactly the same approach as in Re-frame. I can create any function which take reducer as a parameter, do some processing in it then directly call reducer function with the result and call it Reducer Middlewares or whatever. It is just about if I want to make it a bit more easier at the expense of loosing DevTools and more coupling.

I still have to think more if there is an significant advantage in Re-frame approach above some simplification (decreasing amount of actions). I'm open to prove me wrong, as I have said I read to quickly through it.

@vladar

This comment has been minimized.

Show comment
Hide comment
@vladar

vladar Jul 25, 2015

Actually, I was a bit incorrect. The difference is not in Redux vs Re-frame implementations, but really in "where you put your side effects".

If you do this in event handlers (reducers) and disallow action creators to affect your control flow, then technically there is no difference - you can use fsm / statecharts in Redux as well and have your control flow in one place.

But in reality - there is a difference. @gaearon explained it very well: Redux expects your reducers to be pure, otherwise replay feature breaks. So you are not supposed to do that (putting side effects in reducers), as it's against Redux design.

Re-frame also states that event handlers are pure, but even in Readme they mention that HTTP requests are initiated in event handlers. So this is a bit unclear for me, but looks like their expectations are different: you can put your side effects in event-handlers.

Then it's interesting how they approach replay (and that's what I asked at Day8/re-frame#86).

I assume the only way to do it when your event handlers can have side effects is what @tomkis1 mentioned - recording state returned by every event handler (not re-run reducers on every event).

Hot reloading can still work, except that it cannot affect "past" events - only produce some new branch of state in time.

So the difference in design is probably subtle, but as for me it is important.

vladar commented Jul 25, 2015

Actually, I was a bit incorrect. The difference is not in Redux vs Re-frame implementations, but really in "where you put your side effects".

If you do this in event handlers (reducers) and disallow action creators to affect your control flow, then technically there is no difference - you can use fsm / statecharts in Redux as well and have your control flow in one place.

But in reality - there is a difference. @gaearon explained it very well: Redux expects your reducers to be pure, otherwise replay feature breaks. So you are not supposed to do that (putting side effects in reducers), as it's against Redux design.

Re-frame also states that event handlers are pure, but even in Readme they mention that HTTP requests are initiated in event handlers. So this is a bit unclear for me, but looks like their expectations are different: you can put your side effects in event-handlers.

Then it's interesting how they approach replay (and that's what I asked at Day8/re-frame#86).

I assume the only way to do it when your event handlers can have side effects is what @tomkis1 mentioned - recording state returned by every event handler (not re-run reducers on every event).

Hot reloading can still work, except that it cannot affect "past" events - only produce some new branch of state in time.

So the difference in design is probably subtle, but as for me it is important.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 25, 2015

I think they do HTTP requests by composing pure event handlers with side-effecting middlewares. This composition returns new event handler which is not pure anymore but the inner one stays pure. I don't think they suggests to build event handlers which mix app-db (state) mutation and side-effects. It makes code more testable.

If you record result of an event handler you can't make it hot-reloadable which means you can change its code on the fly. You would have to record result of middleware and that would be exactly the same what Redux does - record this result as an action and pass it to reducer (listen to it).

I think I could say that Re-frame middlewares are proactive while Redux allows reactivity (any ad-hoc reducer can listen to a result of middleware/ac). One thing I'm starting to realize is that AC as mostly used in Flux and middlewares are the same thing as controllers.

As I understood @tomkis1 suggests timetravel via saving state, probably on given intervals as I assume it could have perf impact if done on every single mutation.

vladap commented Jul 25, 2015

I think they do HTTP requests by composing pure event handlers with side-effecting middlewares. This composition returns new event handler which is not pure anymore but the inner one stays pure. I don't think they suggests to build event handlers which mix app-db (state) mutation and side-effects. It makes code more testable.

If you record result of an event handler you can't make it hot-reloadable which means you can change its code on the fly. You would have to record result of middleware and that would be exactly the same what Redux does - record this result as an action and pass it to reducer (listen to it).

I think I could say that Re-frame middlewares are proactive while Redux allows reactivity (any ad-hoc reducer can listen to a result of middleware/ac). One thing I'm starting to realize is that AC as mostly used in Flux and middlewares are the same thing as controllers.

As I understood @tomkis1 suggests timetravel via saving state, probably on given intervals as I assume it could have perf impact if done on every single mutation.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 25, 2015

Ok, realizing that reducers/ event handlers return complete new state, hence you said the same.

vladap commented Jul 25, 2015

Ok, realizing that reducers/ event handlers return complete new state, hence you said the same.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 25, 2015

@merk I will try to write my thoughte about it probably on Monday as I probably don't get here during a weekend.

vladap commented Jul 25, 2015

@merk I will try to write my thoughte about it probably on Monday as I probably don't get here during a weekend.

@vladar

This comment has been minimized.

Show comment
Hide comment
@vladar

vladar Jul 25, 2015

Re-frame docs recommend talking to server in event handlers: https://github.com/Day8/re-frame#talking-to-a-server

So this is where it diverges from Redux. And this difference is deeper than it looks at first glance.

vladar commented Jul 25, 2015

Re-frame docs recommend talking to server in event handlers: https://github.com/Day8/re-frame#talking-to-a-server

So this is where it diverges from Redux. And this difference is deeper than it looks at first glance.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 25, 2015

Hm, I'm failing to see the difference from Redux:

The initiating event handlers should organise that the on-success or on-fail handlers for these HTTP requests themselves simply dispatch a new event. They should never attempt to modify app-db themselves. That is always done in a handler.

Replace the event with Action. Redux just have a special name for a pure event handler - a reducer. I think I'm missing something or my brain is down this weekend.

vladap commented Jul 25, 2015

Hm, I'm failing to see the difference from Redux:

The initiating event handlers should organise that the on-success or on-fail handlers for these HTTP requests themselves simply dispatch a new event. They should never attempt to modify app-db themselves. That is always done in a handler.

Replace the event with Action. Redux just have a special name for a pure event handler - a reducer. I think I'm missing something or my brain is down this weekend.

@vladap

This comment has been minimized.

Show comment
Hide comment
@vladap

vladap Jul 25, 2015

I could smell more significant difference how events are executed - queued and async. I think Redux executes actions sync and lock-stepped action by action to guarantee state consistency. It can further affects replayability as I'm not sure if just saving events and replaying them again and again will result in the same end state. Disregarding that I can't just do that because I don't know which events triggers side-effects unlike in Redux where actions always triggers pure code.

vladap commented Jul 25, 2015

I could smell more significant difference how events are executed - queued and async. I think Redux executes actions sync and lock-stepped action by action to guarantee state consistency. It can further affects replayability as I'm not sure if just saving events and replaying them again and again will result in the same end state. Disregarding that I can't just do that because I don't know which events triggers side-effects unlike in Redux where actions always triggers pure code.

@tomkis

This comment has been minimized.

Show comment
Hide comment
@tomkis

tomkis Dec 22, 2015

Contributor

@gaearon I believe @minedeljkovic meant the

For me, this looks as really important addition to redux stack as it allows logic to be primarily in reducers making them an effective and testable state machines, who delegate effects (transition actions whose result does not depend only on current state) to external services (pretty much like Elm architecure, effects and services).

as the main advantage over traditional approach, because https://github.com/yelouafi/redux-saga/ is very similar how redux-thunk works, instead of some syntax sugar on top of it which makes long running transactions easier.

https://github.com/salsita/redux-side-effects on the other hand is more like Elm architecture.

Contributor

tomkis commented Dec 22, 2015

@gaearon I believe @minedeljkovic meant the

For me, this looks as really important addition to redux stack as it allows logic to be primarily in reducers making them an effective and testable state machines, who delegate effects (transition actions whose result does not depend only on current state) to external services (pretty much like Elm architecure, effects and services).

as the main advantage over traditional approach, because https://github.com/yelouafi/redux-saga/ is very similar how redux-thunk works, instead of some syntax sugar on top of it which makes long running transactions easier.

https://github.com/salsita/redux-side-effects on the other hand is more like Elm architecture.

@minedeljkovic

This comment has been minimized.

Show comment
Hide comment
@minedeljkovic

minedeljkovic Dec 22, 2015

Yes, redux-saga and redux-side-effects use generators only to declare side effects, keeping sagas and reducers pure, respectively. That is similar.

Two reasons why I prefer that in reducers:

  1. You have explicit access to current state which may affect how effect should be declared (I think that was one of the @vladar 's points during this discussion)
  2. There is no new concept introduced (Saga in redux-saga)

minedeljkovic commented Dec 22, 2015

Yes, redux-saga and redux-side-effects use generators only to declare side effects, keeping sagas and reducers pure, respectively. That is similar.

Two reasons why I prefer that in reducers:

  1. You have explicit access to current state which may affect how effect should be declared (I think that was one of the @vladar 's points during this discussion)
  2. There is no new concept introduced (Saga in redux-saga)
@tomkis

This comment has been minimized.

Show comment
Hide comment
@tomkis

tomkis Dec 22, 2015

Contributor

My comment in https://github.com/rackt/redux/issues/1139#issuecomment-165419770 is still valid as redux-saga won't solve this and there is no other way to solve this except accepting the model used in Elm architecture.

Contributor

tomkis commented Dec 22, 2015

My comment in https://github.com/rackt/redux/issues/1139#issuecomment-165419770 is still valid as redux-saga won't solve this and there is no other way to solve this except accepting the model used in Elm architecture.

@minedeljkovic

This comment has been minimized.

Show comment
Hide comment
@minedeljkovic

minedeljkovic Dec 24, 2015

I put a simple gist that is trying to emphasize my points about redux-side-effects: https://gist.github.com/minedeljkovic/7347c0b528110889aa50

I tried to define simplest possible scenario that I think is just "real world" enough but still emphasize some of the points from this discussion.

Scenario is:
Application has "User registration" part where personal user data should be entered. Amongst other personal data, country of birth is selected from list of countries. Selection is performed in "Country selection" dialog, where user selects a country and have a choice of confirm or cancel a selection. If user is trying to confirm selection, but no country is selected, she should be warned.

Architecture constraints:

  1. CountrySelection functionality should be modular (in the spirit of redux ducks), so it can be reused in multiple parts of application (e.g. also in "Product administration" part of application, where country of production should be entered)
  2. CountrySelection's slice of state should not be considered global (to be in the root of redux state), but it should be local to module (and controlled by that module) that is invoking it.
  3. Core logic of this functionality needs to include as little moving parts as possible (In my gist this core logic is implemented in reducers only (and only the most important part of the logic). Components should be trivial, as all redux-driven components should be :) . Their only responsibility would be rendering current state, and dispatching actions.)

Most important part of gist, regarding this conversation, is in the way countrySelection reducer is handling CONFIRM_SELECTION action.

@gaearon, I would mostly appriciate your opinion on my statement that redux-side-effects as addition to standard redux provide surface for simplest solution considering constratints.

Possible alternative idea for implementation of this scenario (without using redux-side-effects, but using redux-saga, redux-thunk or some other method), would be also very appriciated.

@tomkis1, I would love to here your opinion, am I using or abusing your library here. :)

(There is slightly different implementation in this gist https://gist.github.com/minedeljkovic/9d4495c7ac5203def688, where globalActions is avoided. It is not important for this topic, but maybe someone would like to comment on this pattern)

minedeljkovic commented Dec 24, 2015

I put a simple gist that is trying to emphasize my points about redux-side-effects: https://gist.github.com/minedeljkovic/7347c0b528110889aa50

I tried to define simplest possible scenario that I think is just "real world" enough but still emphasize some of the points from this discussion.

Scenario is:
Application has "User registration" part where personal user data should be entered. Amongst other personal data, country of birth is selected from list of countries. Selection is performed in "Country selection" dialog, where user selects a country and have a choice of confirm or cancel a selection. If user is trying to confirm selection, but no country is selected, she should be warned.

Architecture constraints:

  1. CountrySelection functionality should be modular (in the spirit of redux ducks), so it can be reused in multiple parts of application (e.g. also in "Product administration" part of application, where country of production should be entered)
  2. CountrySelection's slice of state should not be considered global (to be in the root of redux state), but it should be local to module (and controlled by that module) that is invoking it.
  3. Core logic of this functionality needs to include as little moving parts as possible (In my gist this core logic is implemented in reducers only (and only the most important part of the logic). Components should be trivial, as all redux-driven components should be :) . Their only responsibility would be rendering current state, and dispatching actions.)

Most important part of gist, regarding this conversation, is in the way countrySelection reducer is handling CONFIRM_SELECTION action.

@gaearon, I would mostly appriciate your opinion on my statement that redux-side-effects as addition to standard redux provide surface for simplest solution considering constratints.

Possible alternative idea for implementation of this scenario (without using redux-side-effects, but using redux-saga, redux-thunk or some other method), would be also very appriciated.

@tomkis1, I would love to here your opinion, am I using or abusing your library here. :)

(There is slightly different implementation in this gist https://gist.github.com/minedeljkovic/9d4495c7ac5203def688, where globalActions is avoided. It is not important for this topic, but maybe someone would like to comment on this pattern)

@tomkis

This comment has been minimized.

Show comment
Hide comment
@tomkis

tomkis Dec 28, 2015

Contributor

@minedeljkovic Thanks for the gist. I see one conceptual problem with your example. redux-side-effects is meant to be used for side effects only. In your example however there are no side-effects but rather long living business transaction and therefore redux-saga is much more suitable. @slorber and @yelouafi can shed more light on this.

In other words the most concerning issue for me is synchronous dispatching of new action within reducer (salsita/redux-side-effects#9 (comment)):

yield dispatch => dispatch({type: COUNTRY_SELECTION_SUCCESS, payload: state.selectedCountryId});

I believe @slorber came with the term called "business side effect" and this is exactly your case. redux-saga shines in solving this particular problem.

Contributor

tomkis commented Dec 28, 2015

@minedeljkovic Thanks for the gist. I see one conceptual problem with your example. redux-side-effects is meant to be used for side effects only. In your example however there are no side-effects but rather long living business transaction and therefore redux-saga is much more suitable. @slorber and @yelouafi can shed more light on this.

In other words the most concerning issue for me is synchronous dispatching of new action within reducer (salsita/redux-side-effects#9 (comment)):

yield dispatch => dispatch({type: COUNTRY_SELECTION_SUCCESS, payload: state.selectedCountryId});

I believe @slorber came with the term called "business side effect" and this is exactly your case. redux-saga shines in solving this particular problem.

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Dec 28, 2015

Contributor

@mindjuice i'm not perfectly sure to understand your example but I like the onboarding example I gave here: paldepind/functional-frontend-architecture#20 (comment)

The saga pattern also permits to make some implicit things explicit.
For example you just describe what happened by firing actions (i prefer events because for me they should be in the past tense), and then some business rules kicks in and update the UI. In your case, the display of the error is implicit. With a saga, on confirm, if no country is selected, you would probably dispatch an action "NO_COUNTRY_SELECTED_ERROR_DISPLAYED" or something like that. I prefer that to be totally explicit.

Also you could see the Saga as a coupling point between ducks.
For example you have duck1 and duck2, with local actions on each. If you don't like the idea to couple the 2 ducks (I mean one duck would be using the actionsCreators of the second duck) you could just let them describe what happened, and then create a Saga that wires some complex business rules between the 2 ducks.

Contributor

slorber commented Dec 28, 2015

@mindjuice i'm not perfectly sure to understand your example but I like the onboarding example I gave here: paldepind/functional-frontend-architecture#20 (comment)

The saga pattern also permits to make some implicit things explicit.
For example you just describe what happened by firing actions (i prefer events because for me they should be in the past tense), and then some business rules kicks in and update the UI. In your case, the display of the error is implicit. With a saga, on confirm, if no country is selected, you would probably dispatch an action "NO_COUNTRY_SELECTED_ERROR_DISPLAYED" or something like that. I prefer that to be totally explicit.

Also you could see the Saga as a coupling point between ducks.
For example you have duck1 and duck2, with local actions on each. If you don't like the idea to couple the 2 ducks (I mean one duck would be using the actionsCreators of the second duck) you could just let them describe what happened, and then create a Saga that wires some complex business rules between the 2 ducks.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 7, 2016

Sooooo, it's an insainely long thread and still is there any solution to the problem yet?

Suppose, you have an asynchronous action() and your code must either indicate an error or show the result.

The first approach was to make it like

// the action call
action().then(dispatch(SUCCESS)).catch(dispatch(FAILURE))

// the reducer
case SUCCESS:
    state.succeeded = true
    alert('Success')

case FAILURE:
    state.succeeded = false
    alert('Failure')

But it turns out that it's not Redux-way because now the reducer contains "side effects" (I don't know what that's supposed to mean).
Long story short, the Right way is to move these alert()s out from the reducer somewhere.

That somewhere could be the React component which calls this action.
So now my code looks like:

// the reducer
case SUCCESS:
    state.succeeded = true

case FAILURE:
    state.succeeded = false

// the React component
on_click: function()
{
    action().then
    ({
        dispatch(SUCCESS)

        alert('Success')
        // do something else. maybe call another action
    })
    .catch
    ({
        dispatch(FAILURE)

        alert('Failure')
        // do something else. maybe call another action
    })
}

Is this now the Right way to do it?
Or do I need to investigate more and apply some 3rd party library?

Redux is not simple at all. It's not easy to understand in case of real-world apps. Mabye a sample real-world app repo is needed as a canonical example illustrating the Right way.

catamphetamine commented Jan 7, 2016

Sooooo, it's an insainely long thread and still is there any solution to the problem yet?

Suppose, you have an asynchronous action() and your code must either indicate an error or show the result.

The first approach was to make it like

// the action call
action().then(dispatch(SUCCESS)).catch(dispatch(FAILURE))

// the reducer
case SUCCESS:
    state.succeeded = true
    alert('Success')

case FAILURE:
    state.succeeded = false
    alert('Failure')

But it turns out that it's not Redux-way because now the reducer contains "side effects" (I don't know what that's supposed to mean).
Long story short, the Right way is to move these alert()s out from the reducer somewhere.

That somewhere could be the React component which calls this action.
So now my code looks like:

// the reducer
case SUCCESS:
    state.succeeded = true

case FAILURE:
    state.succeeded = false

// the React component
on_click: function()
{
    action().then
    ({
        dispatch(SUCCESS)

        alert('Success')
        // do something else. maybe call another action
    })
    .catch
    ({
        dispatch(FAILURE)

        alert('Failure')
        // do something else. maybe call another action
    })
}

Is this now the Right way to do it?
Or do I need to investigate more and apply some 3rd party library?

Redux is not simple at all. It's not easy to understand in case of real-world apps. Mabye a sample real-world app repo is needed as a canonical example illustrating the Right way.

@jedwards1211

This comment has been minimized.

Show comment
Hide comment
@jedwards1211

jedwards1211 Jan 7, 2016

@halt-hammerzeit Redux itself it very simple; the confusing fragmentation comes from different needs or opinions about integrating/separating side effects from reducers when using Redux.

jedwards1211 commented Jan 7, 2016

@halt-hammerzeit Redux itself it very simple; the confusing fragmentation comes from different needs or opinions about integrating/separating side effects from reducers when using Redux.

@jedwards1211

This comment has been minimized.

Show comment
Hide comment
@jedwards1211

jedwards1211 Jan 7, 2016

Also, the fragmentation comes from the fact that it's really not that hard to do side effects however you want with Redux. It only took me about 10 lines of code to roll my own basic side effect middleware. So embrace your freedom and don't worry about the "right" way.

jedwards1211 commented Jan 7, 2016

Also, the fragmentation comes from the fact that it's really not that hard to do side effects however you want with Redux. It only took me about 10 lines of code to roll my own basic side effect middleware. So embrace your freedom and don't worry about the "right" way.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 7, 2016

@jedwards1211 "Redux itself" doesn't have any value or meaning and doesn't make any sense because its main purpose is to solve the everyday issues of Flux developers. That implies AJAX, beautiful animations and all the other ordinary and expected stuff

catamphetamine commented Jan 7, 2016

@jedwards1211 "Redux itself" doesn't have any value or meaning and doesn't make any sense because its main purpose is to solve the everyday issues of Flux developers. That implies AJAX, beautiful animations and all the other ordinary and expected stuff

@jedwards1211

This comment has been minimized.

Show comment
Hide comment
@jedwards1211

jedwards1211 Jan 7, 2016

@halt-hammerzeit you have a point there. Redux certainly seems intended to get most people to agree on how to manage state updates, so it's a shame that it hasn't gotten most people to agree on how to perform side effects.

jedwards1211 commented Jan 7, 2016

@halt-hammerzeit you have a point there. Redux certainly seems intended to get most people to agree on how to manage state updates, so it's a shame that it hasn't gotten most people to agree on how to perform side effects.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 7, 2016

Collaborator

Have you seen the "examples" folder in the repo?

While there are multiple ways to perform side effects, generally the recommendation is to either do that outside Redux or inside action creators, like we do in every example.

Collaborator

gaearon commented Jan 7, 2016

Have you seen the "examples" folder in the repo?

While there are multiple ways to perform side effects, generally the recommendation is to either do that outside Redux or inside action creators, like we do in every example.

@jedwards1211

This comment has been minimized.

Show comment
Hide comment
@jedwards1211

jedwards1211 Jan 7, 2016

Yes, I know...all I'm saying is, this thread has illustrated a lack of consensus (at least among people here) about the best way to perform side effects.

jedwards1211 commented Jan 7, 2016

Yes, I know...all I'm saying is, this thread has illustrated a lack of consensus (at least among people here) about the best way to perform side effects.

@jedwards1211

This comment has been minimized.

Show comment
Hide comment
@jedwards1211

jedwards1211 Jan 7, 2016

Though perhaps most users do use thunk action creators and we're just outliers.

jedwards1211 commented Jan 7, 2016

Though perhaps most users do use thunk action creators and we're just outliers.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 7, 2016

^ what he said.

I'd prefer all the greatest minds to agree on a single Righteous solution
and carve it in stone so that i don't have to read through a 9000 screens
thread

On Thursday, January 7, 2016, Andy Edwards notifications@github.com wrote:

Yes, I know...all I'm saying is, this thread has illustrated a lack of
consensus (at least among people here) about the best way to perform side
effects.


Reply to this email directly or view it on GitHub
https://github.com/rackt/redux/issues/291#issuecomment-169751432.

catamphetamine commented Jan 7, 2016

^ what he said.

I'd prefer all the greatest minds to agree on a single Righteous solution
and carve it in stone so that i don't have to read through a 9000 screens
thread

On Thursday, January 7, 2016, Andy Edwards notifications@github.com wrote:

Yes, I know...all I'm saying is, this thread has illustrated a lack of
consensus (at least among people here) about the best way to perform side
effects.


Reply to this email directly or view it on GitHub
https://github.com/rackt/redux/issues/291#issuecomment-169751432.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 7, 2016

So, i guess, the currently agreed dolution is to do rverything in action
creators. I,m gonna try using this approach and incase i find any flaws in
it i'll post back here

On Thursday, January 7, 2016, Николай Кучумов kuchumovn@gmail.com wrote:

^ what he said.

I'd prefer all the greatest minds to agree on a single Righteous solution
and carve it in stone so that i don't have to read through a 9000 screens
thread

On Thursday, January 7, 2016, Andy Edwards <notifications@github.com
javascript:_e(%7B%7D,'cvml','notifications@github.com');> wrote:

Yes, I know...all I'm saying is, this thread has illustrated a lack of
consensus (at least among people here) about the best way to perform side
effects.


Reply to this email directly or view it on GitHub
https://github.com/rackt/redux/issues/291#issuecomment-169751432.

catamphetamine commented Jan 7, 2016

So, i guess, the currently agreed dolution is to do rverything in action
creators. I,m gonna try using this approach and incase i find any flaws in
it i'll post back here

On Thursday, January 7, 2016, Николай Кучумов kuchumovn@gmail.com wrote:

^ what he said.

I'd prefer all the greatest minds to agree on a single Righteous solution
and carve it in stone so that i don't have to read through a 9000 screens
thread

On Thursday, January 7, 2016, Andy Edwards <notifications@github.com
javascript:_e(%7B%7D,'cvml','notifications@github.com');> wrote:

Yes, I know...all I'm saying is, this thread has illustrated a lack of
consensus (at least among people here) about the best way to perform side
effects.


Reply to this email directly or view it on GitHub
https://github.com/rackt/redux/issues/291#issuecomment-169751432.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 7, 2016

Collaborator

This and similar threads exist because 1% of Redux users like to look for more powerful / pure / declarative solutions for side effects. Please don't take the impression that nobody can agree on that. The silent majority uses thinks and promises, and are mostly happy with this approach. But like any tech it has downsides and tradeoffs. If you have complex asynchronous logic you might want to drop Redux and explore Rx instead. Redux pattern is easily expressive in Rx.

Collaborator

gaearon commented Jan 7, 2016

This and similar threads exist because 1% of Redux users like to look for more powerful / pure / declarative solutions for side effects. Please don't take the impression that nobody can agree on that. The silent majority uses thinks and promises, and are mostly happy with this approach. But like any tech it has downsides and tradeoffs. If you have complex asynchronous logic you might want to drop Redux and explore Rx instead. Redux pattern is easily expressive in Rx.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 7, 2016

Ok, thanks

On Thursday, January 7, 2016, Dan Abramov notifications@github.com wrote:

This and similar threads exist because 1% of Redux users like to look for
more powerful / pure / declarative solutions for side effects. Please don't
take the impression that nobody can agree on that. The silent majority uses
thinks and promises, and are mostly happy with this approach. But like any
tech it has downsides and tradeoffs. If you have complex asynchronous logic
you might want to drop Redux and explore Rx instead. Redux pattern is
easily expressive in Rx.


Reply to this email directly or view it on GitHub
https://github.com/rackt/redux/issues/291#issuecomment-169761410.

catamphetamine commented Jan 7, 2016

Ok, thanks

On Thursday, January 7, 2016, Dan Abramov notifications@github.com wrote:

This and similar threads exist because 1% of Redux users like to look for
more powerful / pure / declarative solutions for side effects. Please don't
take the impression that nobody can agree on that. The silent majority uses
thinks and promises, and are mostly happy with this approach. But like any
tech it has downsides and tradeoffs. If you have complex asynchronous logic
you might want to drop Redux and explore Rx instead. Redux pattern is
easily expressive in Rx.


Reply to this email directly or view it on GitHub
https://github.com/rackt/redux/issues/291#issuecomment-169761410.

@jedwards1211

This comment has been minimized.

Show comment
Hide comment
@jedwards1211

jedwards1211 Jan 7, 2016

@gaearon yeah, you're right. And I appreciate that Redux is flexible enough to accommodate all of our different approaches!

jedwards1211 commented Jan 7, 2016

@gaearon yeah, you're right. And I appreciate that Redux is flexible enough to accommodate all of our different approaches!

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Jan 8, 2016

Contributor

@halt-hammerzeit take a look at my answer here where I explain why redux-saga can be better than redux-thunk: http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594

Contributor

slorber commented Jan 8, 2016

@halt-hammerzeit take a look at my answer here where I explain why redux-saga can be better than redux-thunk: http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 8, 2016

@gaearon By the way, your answer on stackoverflow has the same flaw as the documentation - it covers just the simplest case
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
Real-world applications quickly go beyond just fetching data via AJAX: they should also handle errors and perform some actions depending on the output of the action call.
Your advice is to just dispatch some other event and that's it.
So what if my code wants to alert() a user on action completion?
That's where those thunks won't help, but promises will.
I currently write my code using simple promises and it works that way.

I have read the adjacent comment about redux-saga.
Didn't understand what it does though, lol.
I'm not so much into the monads thing and such, and I still don't know what a thunk is, and I don't like that weird word to begin with.

Ok, so I keep using Promises in React components then.

catamphetamine commented Jan 8, 2016

@gaearon By the way, your answer on stackoverflow has the same flaw as the documentation - it covers just the simplest case
http://stackoverflow.com/questions/34570758/why-do-we-need-middleware-for-async-flow-in-redux/34599594#34599594
Real-world applications quickly go beyond just fetching data via AJAX: they should also handle errors and perform some actions depending on the output of the action call.
Your advice is to just dispatch some other event and that's it.
So what if my code wants to alert() a user on action completion?
That's where those thunks won't help, but promises will.
I currently write my code using simple promises and it works that way.

I have read the adjacent comment about redux-saga.
Didn't understand what it does though, lol.
I'm not so much into the monads thing and such, and I still don't know what a thunk is, and I don't like that weird word to begin with.

Ok, so I keep using Promises in React components then.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 8, 2016

Collaborator

We suggest returning promises from thunks throughout the docs.

Collaborator

gaearon commented Jan 8, 2016

We suggest returning promises from thunks throughout the docs.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 8, 2016

@gaearon Yeah, that's what i'm talking about: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
That will do.

catamphetamine commented Jan 8, 2016

@gaearon Yeah, that's what i'm talking about: on_click() { dispatch(load_stuff()).then(show_modal('done')) }
That will do.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 8, 2016

Collaborator

I think you're still missing something.
Please see README of redux-thunk.
It shows how to chain thunk action creators under the "Composition" section.

Collaborator

gaearon commented Jan 8, 2016

I think you're still missing something.
Please see README of redux-thunk.
It shows how to chain thunk action creators under the "Composition" section.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine Jan 8, 2016

@gaearon Yeah, that's exactly what I'm doing:

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});

It's exactly what I wrote above:

on_click()
{
    dispatch(load_stuff())
        .then(() => show_modal('done'))
        .catch(() => show_error('not done'))
}

That README section is well written, thx

catamphetamine commented Jan 8, 2016

@gaearon Yeah, that's exactly what I'm doing:

store.dispatch(
  makeASandwichWithSecretSauce('My wife')
).then(() => {
  console.log('Done!');
});

It's exactly what I wrote above:

on_click()
{
    dispatch(load_stuff())
        .then(() => show_modal('done'))
        .catch(() => show_error('not done'))
}

That README section is well written, thx

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 8, 2016

Collaborator

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 also async example in this repo for more of this.

Collaborator

gaearon commented 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 also async example in this repo for more of this.

@catamphetamine

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine 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:

button_on_click()
{
    this.props.dispatch(load_stuff())
        .then(() =>
        {
                const modal = ReactDOM.getDOMNode(this.refs.modal)
                jQuery(modal).fancy_animation(1200 /* ms */)
                setTimeout(() => jQuery.animate(modal, { background: 'red' }), 1500 /* ms */)
        })
        .catch(() =>
        {
                alert('Failed to bypass the root mainframe protocol to override the function call on the hyperthread's secondary firewall')
        })
}

How would you rewrite it the Right way?

catamphetamine commented 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:

button_on_click()
{
    this.props.dispatch(load_stuff())
        .then(() =>
        {
                const modal = ReactDOM.getDOMNode(this.refs.modal)
                jQuery(modal).fancy_animation(1200 /* ms */)
                setTimeout(() => jQuery.animate(modal, { background: 'red' }), 1500 /* ms */)
        })
        .catch(() =>
        {
                alert('Failed to bypass the root mainframe protocol to override the function call on the hyperthread's secondary firewall')
        })
}

How would you rewrite it the Right way?

@slorber

This comment has been minimized.

Show comment
Hide comment
@slorber

slorber Jan 8, 2016

Contributor

@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/

Contributor

slorber commented 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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Jan 8, 2016

Collaborator

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.)

Collaborator

gaearon commented 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

This comment has been minimized.

Show comment
Hide comment
@catamphetamine

catamphetamine 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.

catamphetamine commented 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

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Mar 18, 2016

Collaborator

Relevant new discussion: #1528

Collaborator

gaearon commented Mar 18, 2016

Relevant new discussion: #1528

@juliandavidmr

This comment has been minimized.

Show comment
Hide comment
@juliandavidmr

juliandavidmr Aug 27, 2016

@gaearon very awesome explanation! 🏆 Thanks 👍

juliandavidmr commented Aug 27, 2016

@gaearon very awesome explanation! 🏆 Thanks 👍

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.