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

Solution for simple action creation without string constants and less magic #628

Closed
grigory-leonenko opened this Issue Aug 26, 2015 · 12 comments

Comments

4 participants
@grigory-leonenko

grigory-leonenko commented Aug 26, 2015

I was little annoyed by string constants as action types. And write solution for this, need for objective criticism and ideas. The solution is create action as class methods, and make method name accesible.

@EasyActions
class Actions {
   USER_ACTION(type, id){
        return {type, id}
   }
}
export default new Actions(); 

---
import Action from './actions';

dispatch(Actions.USER_ACTION(42))

switch(action.type) {
   case Actions.USER_ACTION:
       // return state
}

More info and examples here: https://github.com/grigory-leonenko/redux-easy-actions

@grigory-leonenko

This comment has been minimized.

Show comment
Hide comment
@grigory-leonenko

grigory-leonenko Aug 26, 2015

Thanks. I explore this solutions before. But i wanted to keep action creators as plain function without any magic and don't use string constants or names.

grigory-leonenko commented Aug 26, 2015

Thanks. I explore this solutions before. But i wanted to keep action creators as plain function without any magic and don't use string constants or names.

@sodiumjoe

This comment has been minimized.

Show comment
Hide comment
@sodiumjoe

sodiumjoe Sep 3, 2015

@gaearon It seems to me that the types as strings allow for a lot of opportunity for error, and action type constants are a lot of unnecessary boilerplate. I really like redux, it seems like a distillation of all the good parts of "canonical" flux, but dispatching actions by strings seems like another vestigial thing from canonical flux that isn't really necessary.

I also like the direction @grigory-leonenko is going. We've actually implemented our own flux-ish singleton store on top of baobab, and we've come to a lot of the same conclusions that redux has. However, we did get rid of dispatching by strings and replaced them with simple function calls.

We have an "actions definition" object where we define the equivalent of reducers:

let actions = {
  setFoo(context, payload) { ... },
  setBar(context, payload) { ... }
};

These definitions get partially applied with the appropriate context and the store exposes the partially applied functions on its actions property:

let store = new Store({ initialState, actions });
store.actions.setFoo('baz');

This has the nice property of not relying on string constants to match on, and not having to define/bikeshed over the name of the property to match on, or to have to handle actions that don't match on a type. Misspellings generate a simple 'not a function' exception.

We've actually gone a bit further than this, too. To solve the issue of "name-spacing", e.g. inventing a naming scheme for actions on large store with action types having names like "FOO:BAR:INCREMENT" or whatever, we instead allow the action tree to mirror the shape of the state tree:

let initialState = {
  foo: {
    items: []
  },
  bar: {
    items: []
  }
};

let actions = {
  foo: {
    items: {
      addItem(context, payload) {
       return context.getState().concat(payload);
      }
    }
  }
};

In this example, context is the "path" to the function in action tree (foo.items here).

You can see the obvious opportunity for composing a reducer into the two separate namespaces.

This also gives us nice, namespaced access to the actions:

store.actions.foo.items.addItem({ id: 0 });

I'm investigating replacing our own store implementation with redux, and I'm leaning toward doing it anyway, but these are some ergonomic niceties we've enjoyed in our implementation that I would love to see (or something similar) in redux. The implementation would be fairly trivial, but it is a pretty significant departure from the current redux api.

In any case, I appreciate any feedback. Keep up the great work!

sodiumjoe commented Sep 3, 2015

@gaearon It seems to me that the types as strings allow for a lot of opportunity for error, and action type constants are a lot of unnecessary boilerplate. I really like redux, it seems like a distillation of all the good parts of "canonical" flux, but dispatching actions by strings seems like another vestigial thing from canonical flux that isn't really necessary.

I also like the direction @grigory-leonenko is going. We've actually implemented our own flux-ish singleton store on top of baobab, and we've come to a lot of the same conclusions that redux has. However, we did get rid of dispatching by strings and replaced them with simple function calls.

We have an "actions definition" object where we define the equivalent of reducers:

let actions = {
  setFoo(context, payload) { ... },
  setBar(context, payload) { ... }
};

These definitions get partially applied with the appropriate context and the store exposes the partially applied functions on its actions property:

let store = new Store({ initialState, actions });
store.actions.setFoo('baz');

This has the nice property of not relying on string constants to match on, and not having to define/bikeshed over the name of the property to match on, or to have to handle actions that don't match on a type. Misspellings generate a simple 'not a function' exception.

We've actually gone a bit further than this, too. To solve the issue of "name-spacing", e.g. inventing a naming scheme for actions on large store with action types having names like "FOO:BAR:INCREMENT" or whatever, we instead allow the action tree to mirror the shape of the state tree:

let initialState = {
  foo: {
    items: []
  },
  bar: {
    items: []
  }
};

let actions = {
  foo: {
    items: {
      addItem(context, payload) {
       return context.getState().concat(payload);
      }
    }
  }
};

In this example, context is the "path" to the function in action tree (foo.items here).

You can see the obvious opportunity for composing a reducer into the two separate namespaces.

This also gives us nice, namespaced access to the actions:

store.actions.foo.items.addItem({ id: 0 });

I'm investigating replacing our own store implementation with redux, and I'm leaning toward doing it anyway, but these are some ergonomic niceties we've enjoyed in our implementation that I would love to see (or something similar) in redux. The implementation would be fairly trivial, but it is a pretty significant departure from the current redux api.

In any case, I appreciate any feedback. Keep up the great work!

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 3, 2015

Contributor

I really like redux, it seems like a distillation of all the good parts of "canonical" flux, but dispatching actions by strings seems like another vestigial thing from canonical flux that isn't really necessary.

Have you had a chance to read this?
Redux doesn't have a concept of “string constants”. Whether they are constants, or generated strings, or anything else is up to you. The only thing Redux cares about is actions being serializable.

Describing Redux as having “action type string constants” is as meaningless as describing a physics calculation function as using “integer constants”. Constants is what you write in your code—or generate if you like. By the point they get to Redux, they're just strings. How you get those strings is entirely your concern.

Contributor

gaearon commented Sep 3, 2015

I really like redux, it seems like a distillation of all the good parts of "canonical" flux, but dispatching actions by strings seems like another vestigial thing from canonical flux that isn't really necessary.

Have you had a chance to read this?
Redux doesn't have a concept of “string constants”. Whether they are constants, or generated strings, or anything else is up to you. The only thing Redux cares about is actions being serializable.

Describing Redux as having “action type string constants” is as meaningless as describing a physics calculation function as using “integer constants”. Constants is what you write in your code—or generate if you like. By the point they get to Redux, they're just strings. How you get those strings is entirely your concern.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 3, 2015

Contributor

we instead allow the action tree to mirror the shape of the state tree:

If actions could always mirror the state tree, there would be no use in Redux (or Flux) because you'd just update the immutable tree directly. In real, large apps, the shape of the state tree rarely corresponds intuitively to the way you think about it. For example, you might want to express “follow user” action, but in reality, it corresponds to updates to state.followersByUserId[followedUserId], state.followedByUserId[currentUserId], state.users[followedUserId], and state.users[currentUserId]. As the app grows, the same “simple” action may correspond to entirely unrelated state updates. That's the problem Redux and Flux are solving, letting you separate the descriptions of these updates (reducers), while keeping a single way to cause them (action).

Contributor

gaearon commented Sep 3, 2015

we instead allow the action tree to mirror the shape of the state tree:

If actions could always mirror the state tree, there would be no use in Redux (or Flux) because you'd just update the immutable tree directly. In real, large apps, the shape of the state tree rarely corresponds intuitively to the way you think about it. For example, you might want to express “follow user” action, but in reality, it corresponds to updates to state.followersByUserId[followedUserId], state.followedByUserId[currentUserId], state.users[followedUserId], and state.users[currentUserId]. As the app grows, the same “simple” action may correspond to entirely unrelated state updates. That's the problem Redux and Flux are solving, letting you separate the descriptions of these updates (reducers), while keeping a single way to cause them (action).

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 3, 2015

Contributor

(I'm closing since this got some feedback & is easily findable in issues.)

Contributor

gaearon commented Sep 3, 2015

(I'm closing since this got some feedback & is easily findable in issues.)

@gaearon gaearon closed this Sep 3, 2015

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 3, 2015

Contributor

However, we did get rid of dispatching by strings and replaced them with simple function calls.

This is exactly what this doc section talks about: http://rackt.github.io/redux/docs/recipes/ReducingBoilerplate.html#actions

Actions are plain objects describing what happened in the app, and serve as the sole way to describe an intention to mutate the data. It’s important that actions being objects you have to dispatch is not boilerplate, but one of the fundamental design choices of Redux.

There are frameworks claiming to be similar to Flux, but without a concept of action objects. In terms of being predictable, this is a step backwards from Flux or Redux. If there are no serializable plain object actions, it is impossible to record and replay user sessions, or to implement hot reloading with time travel. If you’d rather modify data directly, you don’t need Redux.

It is a common convention that actions have a constant type that helps reducers (or Stores in Flux) identify them. We recommend that you use strings and not Symbols for action types, because strings are serializable, and by using Symbols you make recording and replaying harder than it needs to be.

In Flux, it is traditionally thought that you would define every action type as a string constant.

Why is this beneficial? It is often claimed that constants are unnecessary, and for small projects, this might be correct. For larger projects, there are some benefits to defining action types as constants:

  • It helps keep the naming consistent because all action types are gathered in a single place.
  • Sometimes you want to see all existing actions before working on a new feature. It may be that the action you need was already added by somebody on the team, but you didn’t know.
  • The list of action types that were added, removed, and changed in a Pull Request helps everyone on the team keep track of scope and implementation of new features.
  • If you make a typo when importing an action constant, you will get undefined. This is much easier to notice than a typo when you wonder why nothing happens when the action is dispatched.

It is up to you to choose the conventions for your project. You may start by using inline strings, and later transition to constants, and maybe later group them into a single file. Redux does not have any opinion here, so use your best judgment.

Contributor

gaearon commented Sep 3, 2015

However, we did get rid of dispatching by strings and replaced them with simple function calls.

This is exactly what this doc section talks about: http://rackt.github.io/redux/docs/recipes/ReducingBoilerplate.html#actions

Actions are plain objects describing what happened in the app, and serve as the sole way to describe an intention to mutate the data. It’s important that actions being objects you have to dispatch is not boilerplate, but one of the fundamental design choices of Redux.

There are frameworks claiming to be similar to Flux, but without a concept of action objects. In terms of being predictable, this is a step backwards from Flux or Redux. If there are no serializable plain object actions, it is impossible to record and replay user sessions, or to implement hot reloading with time travel. If you’d rather modify data directly, you don’t need Redux.

It is a common convention that actions have a constant type that helps reducers (or Stores in Flux) identify them. We recommend that you use strings and not Symbols for action types, because strings are serializable, and by using Symbols you make recording and replaying harder than it needs to be.

In Flux, it is traditionally thought that you would define every action type as a string constant.

Why is this beneficial? It is often claimed that constants are unnecessary, and for small projects, this might be correct. For larger projects, there are some benefits to defining action types as constants:

  • It helps keep the naming consistent because all action types are gathered in a single place.
  • Sometimes you want to see all existing actions before working on a new feature. It may be that the action you need was already added by somebody on the team, but you didn’t know.
  • The list of action types that were added, removed, and changed in a Pull Request helps everyone on the team keep track of scope and implementation of new features.
  • If you make a typo when importing an action constant, you will get undefined. This is much easier to notice than a typo when you wonder why nothing happens when the action is dispatched.

It is up to you to choose the conventions for your project. You may start by using inline strings, and later transition to constants, and maybe later group them into a single file. Redux does not have any opinion here, so use your best judgment.

@sodiumjoe

This comment has been minimized.

Show comment
Hide comment
@sodiumjoe

sodiumjoe Sep 4, 2015

Thanks for engaging with what may be some dumb or incompatible ideas. I appreciate it!

However, it seems I've misrepresented some ideas. I have read through the docs, which I'd like to applaud your documentation, it's truly top-notch, and one of the reasons we're probably going to switch.

There's very little about the guiding principles behind redux that I disagree with. Most of my concerns are ergonomic in nature, I think.

Redux doesn't have a concept of “string constants”. Whether they are constants, or generated strings, or anything else is up to you. The only thing Redux cares about is actions being serializable.

I definitely agree with the idea of serializable actions and the benefits you get from them, but I'm not sure the "action object" as such is the only or best way to achieve those. In my proposed change, you could easily generate a serializable object from the context and name of the action, to generate the equivalent "history" of state mutations. (Currently we let baobab handle mutation history for us.)

If actions could always mirror the state tree, there would be no use in Redux (or Flux) because you'd just update the immutable tree directly.

imho, it is still useful to isolate all possible state mutations and colocate them with the store definition, rather than interleave state mutation logic with e.g. the view layer.

I think your point also gets at another perhaps fundamental difference in how we're managing state in our implementation. Specifically:

We suggest that you keep your state as normalized as possible, without any nesting.

Instead, we model our state nested in a way that makes sense for the application. That being the case, generally when it's appropriate for an action to mutate multiple nodes in the store, there's a common parent node in the state tree where the action can live:

let state = {
  foo: {
    bar: [],
    baz: [],
  }
};

let actions = {
  foo: {
    resetAll() {
      return { bar: [], baz: [] };
    },
    bar: {
      reset() {
        return [];
      }
    }
  }
};

We also avoid a lot of complexity in our actual action definition logic with the concept of "view state". I.e., we have "canonical state" in our store, which is mostly view-agnostic, and a pure computeViewState function which takes the canonical state and computes a state that's more easily consumable by the view.

So, if I understand your example correctly, the "follow user" action would only have to modify state.users[currentUserId]. The rest would be computed in the computeViewState function.

What this has meant for us so far is that state mutations are generally isolated to a fairly granular namespace, and we avoid a lot of potential for inconsistent data (how do you enforce that all of the interrelated state fields are updated in concert?).

In any case, thanks for bearing with me. If I'm totally barking up the wrong tree, please let me know and I'll drop this.

sodiumjoe commented Sep 4, 2015

Thanks for engaging with what may be some dumb or incompatible ideas. I appreciate it!

However, it seems I've misrepresented some ideas. I have read through the docs, which I'd like to applaud your documentation, it's truly top-notch, and one of the reasons we're probably going to switch.

There's very little about the guiding principles behind redux that I disagree with. Most of my concerns are ergonomic in nature, I think.

Redux doesn't have a concept of “string constants”. Whether they are constants, or generated strings, or anything else is up to you. The only thing Redux cares about is actions being serializable.

I definitely agree with the idea of serializable actions and the benefits you get from them, but I'm not sure the "action object" as such is the only or best way to achieve those. In my proposed change, you could easily generate a serializable object from the context and name of the action, to generate the equivalent "history" of state mutations. (Currently we let baobab handle mutation history for us.)

If actions could always mirror the state tree, there would be no use in Redux (or Flux) because you'd just update the immutable tree directly.

imho, it is still useful to isolate all possible state mutations and colocate them with the store definition, rather than interleave state mutation logic with e.g. the view layer.

I think your point also gets at another perhaps fundamental difference in how we're managing state in our implementation. Specifically:

We suggest that you keep your state as normalized as possible, without any nesting.

Instead, we model our state nested in a way that makes sense for the application. That being the case, generally when it's appropriate for an action to mutate multiple nodes in the store, there's a common parent node in the state tree where the action can live:

let state = {
  foo: {
    bar: [],
    baz: [],
  }
};

let actions = {
  foo: {
    resetAll() {
      return { bar: [], baz: [] };
    },
    bar: {
      reset() {
        return [];
      }
    }
  }
};

We also avoid a lot of complexity in our actual action definition logic with the concept of "view state". I.e., we have "canonical state" in our store, which is mostly view-agnostic, and a pure computeViewState function which takes the canonical state and computes a state that's more easily consumable by the view.

So, if I understand your example correctly, the "follow user" action would only have to modify state.users[currentUserId]. The rest would be computed in the computeViewState function.

What this has meant for us so far is that state mutations are generally isolated to a fairly granular namespace, and we avoid a lot of potential for inconsistent data (how do you enforce that all of the interrelated state fields are updated in concert?).

In any case, thanks for bearing with me. If I'm totally barking up the wrong tree, please let me know and I'll drop this.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 4, 2015

Contributor

In my proposed change, you could easily generate a serializable object from the context and name of the action, to generate the equivalent "history" of state mutations. (Currently we let baobab handle mutation history for us.)

I don't argue with this. I was responding to “we did get rid of dispatching by strings”. Whichever way you get those strings (constants, generating function names, etc) is your business. The only thing Redux cares about is they're strings at the end by the time you dispatch an action. Whether you use constants or generate strings from function name is not really relevant for Redux—it doesn't care. :-) This is the part of Redux that is completely up to you and does not warrant any changes to Redux itself.

We also avoid a lot of complexity in our actual action definition logic with the concept of "view state". I.e., we have "canonical state" in our store, which is mostly view-agnostic, and a pure computeViewState function which takes the canonical state and computes a state that's more easily consumable by the view.

This is precisely what I'm advocating. Keep your state normalized, and have functions that compute "view state" when you need it to be tree-shaped. You can memoize them as described in Computing Derived Data.

Contributor

gaearon commented Sep 4, 2015

In my proposed change, you could easily generate a serializable object from the context and name of the action, to generate the equivalent "history" of state mutations. (Currently we let baobab handle mutation history for us.)

I don't argue with this. I was responding to “we did get rid of dispatching by strings”. Whichever way you get those strings (constants, generating function names, etc) is your business. The only thing Redux cares about is they're strings at the end by the time you dispatch an action. Whether you use constants or generate strings from function name is not really relevant for Redux—it doesn't care. :-) This is the part of Redux that is completely up to you and does not warrant any changes to Redux itself.

We also avoid a lot of complexity in our actual action definition logic with the concept of "view state". I.e., we have "canonical state" in our store, which is mostly view-agnostic, and a pure computeViewState function which takes the canonical state and computes a state that's more easily consumable by the view.

This is precisely what I'm advocating. Keep your state normalized, and have functions that compute "view state" when you need it to be tree-shaped. You can memoize them as described in Computing Derived Data.

@gaearon

This comment has been minimized.

Show comment
Hide comment
@gaearon

gaearon Sep 4, 2015

Contributor

Please forgive me for my withdrawing from this discussion because it comes up every now and then, and always goes nowhere. I created redux/examples/real-world example to show the power of reducer composition pattern. (See how it handles pagination.) If you know a better way to structure such mutations, please show by example :-). A trivial example doesn't help because it skips all the important details. Ideally I'd like to see each proposal to enhance Redux as a pull request to real-world example, showing how that example will be simplified as a result.

Contributor

gaearon commented Sep 4, 2015

Please forgive me for my withdrawing from this discussion because it comes up every now and then, and always goes nowhere. I created redux/examples/real-world example to show the power of reducer composition pattern. (See how it handles pagination.) If you know a better way to structure such mutations, please show by example :-). A trivial example doesn't help because it skips all the important details. Ideally I'd like to see each proposal to enhance Redux as a pull request to real-world example, showing how that example will be simplified as a result.

@sodiumjoe

This comment has been minimized.

Show comment
Hide comment
@sodiumjoe

sodiumjoe Sep 5, 2015

No need to apologize, I appreciate the responses! I'll take a look at the real-world example. Thanks!

sodiumjoe commented Sep 5, 2015

No need to apologize, I appreciate the responses! I'll take a look at the real-world example. Thanks!

@brunolm

This comment has been minimized.

Show comment
Hide comment
@brunolm

brunolm Aug 1, 2016

I created a different approach brunolm/ts-react-redux-startup@aa9d555

Basically it works the same way, but constant values are created automatically and there is no need to prefix constants. (Also if you are using TypeScript the object reference is still there, so it is really easy to rename it if you want to)

brunolm commented Aug 1, 2016

I created a different approach brunolm/ts-react-redux-startup@aa9d555

Basically it works the same way, but constant values are created automatically and there is no need to prefix constants. (Also if you are using TypeScript the object reference is still there, so it is really easy to rename it if you want to)

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