reduxjs/redux

examples - how to implement action type namespacing ?

pbc opened this issue Β· 32 comments

pbc commented

I have spent about a week now trying to find any good implementations of action type namespacing and have rolled my own which is based on Transition classes which hold definition of an action creator, reducer and a list of it's child transitions, but this approach requires way too much boiler plate code and it's problematic when you have a lot of very simple actions.

In all of the examples which you can find in this repo and plenty of others this problem is always ignored for some reason but this was the first problem I have spotted when started looking into Redux.

Am I missing something here ? You guys have no action type naming collisions ?

Can someone update the examples and show me how you deal with this problem or point me in the right direction ?

My specific case is related to having 2 separate admin panels on the frontend. One for testers and one for customers which will have their state stored in separate sections of the store ("testerAccount", "customerAccount"), but both of them will be able to do similar things for e.g. ADD_VIDEO_UPLOAD, ADD_COMMENT, etc, etc.

I would really appreciate your help here :)

Put accountType into actions? See also examples/real-world/reducers for how you can write a reducer factory and use the same code many times producing reducers that respond to different actions.

Similarly don't forget action creators are just functions, and you can create functions that return a bunch of other functions.

I think it would really help if you provided a specific small example of the problem you say is ignored in existing examples.

We can then help you find ways to simplify that specific example. It's very hard to suggest something specific in response to a question without code.

pbc commented

@gaearon I know where you're heading with this, but I am not trying to create reusable reducers, action creators or DRY things up, quite the opposite.

I am trying to isolate groups of actions from each other so I can have 2 people working on two different sections of the system and be sure that they won't start triggering each other's reducers accidentally due to lack of namespacing in the action type names.

Hope this makes it more clear.

I've solved this issue in my project(s) by using https://www.npmjs.com/package/flux-constant in combination with a super simple helper function called createActionTypes.

Essentially, my code ends up being:

// create-action-types.js
var fluxConstant = require('flux-constant');
module.exports = function (types) {
    return fluxConstant.set(types);
};

// in foo-action-types.js
module.exports = createAtionTypes([
    'ADD_FOO',
    'REMOVE_FOO'
]);

// in some-store.js
var fooActionTypes = require('foo-action-types');
function (state, action) {
    switch(action.type) {
        case fooActionTypes.ADD_FOO: 

        case fooActionTypes.REMOVE_FOO:
   }
}

Why not keep all action types as constants in one place?
Then they sure can't clash because you can't export the same name twice.

pbc commented

To better visualise this, let's say this is the initial state of the store:

let initialState = {
  "testerAccount": {
    "videoUploads": [],
    "messages": []
  },
  "customerAccount": {
    "videoUploads": [],
    "messages": []
  },
  "systemUserAccount": {
    "videoUploads": [],
    "messages": []
  }
};

How will you avoid action type name collisions when implementing separate reducers for adding videos or messages for each of these sections ?

pbc commented

@gaearon what you're suggesting doesn't solve the core of the problem, it's more of duct tape approach because over time you will end up with a huge type file which will cause a lot of problems .

It will straight away cause problems during code merges which later on will need to be solved with naming hacks, for e.g.:

ADD_VIDEO_UPLOAD, ADD_TESTER_VIDEO_UPLOAD, ADD_VIDEO_UPLOAD_IN_SOME_SECTION, etc.

which is a one big headache again.

@koulmomo This is exactly what I was looking for, thank you :) πŸ‘ Simple and powerful.

@gaearon I think we should have at least one example which uses either this package or a new package which could be named redux-constant ?

In my opinion promoting approach in the docs / examples, which solves the problem of namespacing should be the new default.

Thank you for your help with this :)

In your example, I don't really get why not have one set of types, one reducer generator and one set of action creators. The actions would contain accountType property to tell them apart. Action creators would accept it as a parameter. The reducer factory would accept accountType and return a reducer that only handles actions with this account type.

I don't think flux-constant is a great solution here. It seems to rely on instanceof checks. Which means you can't serialize your actions and later replay them becauseβ€”bam!β€”their deserialized types won't match the generated ones.

People often try to make Flux β€œsimpler” without realizing they are breaking its essential features.

In your example, I don't really get why not have one set of types, one reducer generator and one set of action creators. The actions would contain accountType property to tell them apart. Action creators would accept it as a parameter. The reducer factory would accept accountType and return a reducer that only handles actions with this account type.

I was mislead by the apparent symmetry in your example. I understand now that you meant there is no DRY here, and the symmetry in features is only apparent as you said in #786 (comment).

I don't think there's a need for a library here. Establish a convention! For example, if your subprojects or features are so separate, make it a rule to call action types feature/ACTION_TYPE, e.g. testers/UPLOAD_VIDEO or customers/UPLOAD_VIDEO. This is especially nice if these β€œgroups” actually correspond to real folders (or betterβ€”packages) on the disk.

Violations will be easy to catch in code reviews. If you really want to, you can automate this but I can't see what it brings over manual namespacing. Inside each module, you'd still want to declare all constants in one file for easier control over features, preventing accidental duplication of effort, and as a documentation.

I'm all for adding a huge-apps example with code splitting, namespaced action types, etc. That would be a separate issue though.

Strings aren't inherently bad for namespacing. URLs are strings, and they seem to work fine.

pbc commented

@gaearon You are right , I forgot about the problem of serialization with the solution based on flux-constant.

I don't have a problem with string based namespacing as long as we can easily distinguish the modules / namespaces in the name it self.

The naming convention based solution is something I have seen before:

https://github.com/erikras/ducks-modular-redux

but I hoped that there might be a better or a "correct" way of doing this which would be based on your experience.

I think I will try to convert the flux-constant approach into something that you could actually serialize in some way.

Please add a "huge-apps" example if you find time for this because this will save other people a lot of time and maybe it will lead to establishing some sort of convention which we could easily follow later on.

Thank you again :)

Yeah, sorry, unfortunately I haven't been working on a big app for some time, and even when I did, I actually liked that we have a single giant file with constants grouped into sections because it provides a good overview of what may happen in the app.

I'd look to see this "problem" explained in more detail in the docs. I have/had the same questions like @pbc. I don't think "Why not keep all action types as constants in one place?" works if you re-use multiple modules across projects and the recommendation "Establish a convention!" sounds very much like something like BEM in CSS land. However we move away from BEM to CSS modules and I guess something similar to CSS modules which hash class names is needed for action types in big projects, too.

As far as I can tell, there is no way to achieve both serialization and absence of conflicts.

A good convention for reusable modules might use already existing "uniqueness providers", like reverse domains names, or username/repo on github, or registered module name on npm.


EDIT: CSS modules does this by defining a custom language on top of CSS and prefixing the classnames during preprocessing (boils down to convention anyway, but a generated one).

+1 for the question and the discussion. I also struggled a lot with this exact problem as @pbc . Confusion for me is that we have combineReducers for reducers, which makes a nice state tree, but actions on the other hand feel more or less like a global. By the looks of it, any type of namespacing has to be done manually.

I think I've found a solution for serializable string action types. I think it's one of those situation where our minds impose artificial limitations on something.

The basic idea is that the type constants don't need to be related in any way to the string value. So you can use randomly generated values, hashed file paths, or anything else unique for the type constant string value. In your reducer, you import the type constant by name, and compare it by name. That name might be used by other actions and in other reducers, but it doesn't matter, because the value won't be the same.

Example here: https://gist.github.com/samsch/63a54e868d7fa2b6023a

This is sensible. You still want to retain some human readable part in the action name, but generating unique prefixes totally works.

If you can prove that they are unique, of course.
The best way is still to use already existing uniqueness providers: reverse domain names or npm module names.

Namespacing with convention has its perils when you are writing modules than can be used in various places that is outside your control.

Lets say you have a module "Geometry", with ActionType of "Area". This module is used in two places:

  1. AppA->Drawing->Geometry (namespace="Drawing/Geometry/Area")
  2. AppB->Trigonometry->Shape->Geometry (namespace="Trigonometry/Shape/Area")

Now you have two conflicting namespace, depending on where your module gets used.

  • It is not a good idea to hardcode this full path in your Geometry module for ActionType "Area".
  • Rather, keep the name simple: "Area".
  • In a similar fashion to reducer composition, compose the namespace by having each containing parent to add a prefix.

I am experimenting with the following pattern:

create a dir for each type of collection that contains:

  • consts
  • reducer
  • components
  • sagas

create consts in following format:

// song-store/song-store-consts.js
export const ADD = 'SONG_STORE.ADD'
export const REMOVE = 'SONG_STORE.REMOVE'

When using constants in reducers, saga or actions import them all with *:

// song-store/song-store-actions.js
import * as SONG_STORE from './song-store-consts'

export function addSongStore(name) {
  return {
    type: SONG_STORE.ADD,
    name
  }
}

export function removeSongStore(songStoreId) {
  return {
    type: SONG_STORE.REMOVE,
    songStoreId
  }
}

Sadly not great for tree shaking. Would be nice if ES6 allowed:

import { ADD, REMOVE } as SONG_STORE from './song-store-actions'

Anyone know if Webpack 2 can intelligently tree shake import * so it does not bundle code for exports that are not used even if imported with *?

This seems to be a very common issue and it pops up around our office often, either:

  • Name conflicts cause undesirable behaviour.
  • Complaining about the boilerplate associated with creating unique action types.

We tried a few different solutions but nothing seemed to stick:

  1. Keeping all the action types as constants in one place certainly makes it easier to manage, but I found it becomes a little unwieldy when the app grows.
  2. Randomly generated values mentioned by @samsch really caught my attention and it certainly works, but yes, losing the human readable part makes it more difficult to live with.
  3. The comment above by @philholden really appealed to me, as we typically use a feature driven architecture and the directory structure is quite descriptive.

After experimenting for the last few months, we decided to use the directory structure to namespace our action types. Typing these out manually gets old really fast, so I attempted to do something with __filename but this doesn't work due to code bundling etc. Then I made my first Babel plugin which transforms the keyword __filenamespace into a static string, which seems to be working well.

Example

In App/testerAccount/index.js:

// Something like this
const ADD_VIDEO_UPLOAD = `${__filenamespace}/ADD_VIDEO_UPLOAD`;
const ADD_COMMENT = `${__filenamespace}/ADD_COMMENT`;

// Will be transformed into something like this
const ADD_VIDEO_UPLOAD = 'App/testerAccount/ADD_VIDEO_UPLOAD';
const ADD_COMMENT = 'App/testerAccount/ADD_COMMENT';

Feel free to give it a try, I hope it's useful. It would be fantastic to get any feedback, especially from @pbc or @gaearon as the issue creator and library creator.

https://www.npmjs.com/package/babel-plugin-filenamespace

I was really hoping someone would offer up a "standard". We don't need symbols or objects for this, we just need a convention.

Are we going to do "featureName$actionType" or "fileName/ACTION_TYPE" or "PROJECT.FEATURE.ACTION"? If we can all agree on something, it will be easier to share reducers.

I think with the dearth of other available conventions, Ducks has become the defacto standard.

const ACTION = 'app/feature/ACTION'; is pretty sufficient.

ivks commented

@gaearon has always mentioned that 1:1 relation between actions and reducers is probably a bad idea and I really do agree. But what about the case where two different views actually want to call the same reducer.
For eg. A simple preference toggle could be enabled or disabled from two different places:
/myaccount/toggleNotification
/dashboard/toggleNotification
so should we have two reducers written with the same content in
reducers/notifications.js
cc: @samit4me , @philholden

<Edit 1>
Just on another thought, I think this is a good idea, of having two or more reducers within a file doing the same job with different action names. This is how by just seeing the reducer we can understand that from how many different places a particular state is getting modified.

Anyways, would love to hear from the experts. Thanks.

@ivks : several thoughts here.

First, you can dispatch the same action from multiple places within the application.

Second, you can definitely have the same reducer listening for multiple action types, and handling them the same way.

Third, the "1:1 relation" aspect is about being able to have multiple slice reducers listening to the same action, with each one updating its bit of state independently. Yes, much of the time there might only be one reducer that cares about a given action, but having multiple reducers respond is very much an intended use case for Redux.

ivks commented

@markerikson

Second, you can definitely have the same reducer listening for multiple action types, and handling them the same way.

I actually was using redux-actions, and did not know it has a utility method combineAction which will do the task. Just found it and your above statement just mention the same. (should have been more clear, sorry)
Thank you very much for replying.

I don't think Ducks defines correctly in ActionTypes:

MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE

The reason is one action may be subscribed by more than one reducers. As @mherodev said, const ACTION = 'app/feature/ACTION'; makes more sense, additionally I'd like to add a schema for action: const ACTION = 'action://app/feature/ACTION';.

Why not just use a globally self-incremental integer to identify the action types? e.g.

let id = 0

function generateActionType (label /* for readability */) {
  id++
  return `app/feature/${id}/${label}`
}

Thought about this a bit, to make sure we don't clash with other developers, we're writing a lint rule, which looks into a directory (where we've got all reducers and each reducers have their own action types as constants)

We're planning on linting such that it reports a violation if there are two constants with same value.

If there's any suggestion, please let me know :)