Skip to content
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

An alternative way to handle actions and reducers #2378

Closed
acailly opened this issue Apr 28, 2017 · 4 comments
Closed

An alternative way to handle actions and reducers #2378

acailly opened this issue Apr 28, 2017 · 4 comments

Comments

@acailly
Copy link

acailly commented Apr 28, 2017

Hi redux community,

I would like to have some feedback on a syntax I just started to use and which I find both simpler and more expressive.
But first a disclaimer, I know this is not redux by the book, but I find it respects the core principles enough to worth a discusion 😉

I tested many approaches:

Technical oriented structure 🔨

All actions in an 'actions' folder, all reducers in a 'reducers' folder.

actions
|_ todos.js
|_ users.js

reducers
|_ todos.js
|_ users.js

Great to get all actions in one import and pass them to connected component.

Not great when it comes to change a requirement because many files must be changed and they are not in the same place.

Domain oriented structure 🎓

As best explained here, it consists in grouping files by domain concepts instead of technical concepts: https://marmelab.com/blog/2015/12/17/react-directory-structure.html

todos
|_ todos.reducer.js
|_ todos.actions.js

users
|_ users.reducer.js
|_ users.actions.js

Great to express what the application is about.

A bit verbose maybe?

Here comes the ducks 🦆🦆🦆

The explanation is here: https://github.com/erikras/ducks-modular-redux

The idea is basically to group together the actions and the reducer in one file.

modules
|_ todos.js
|_ users.js

Great because it's not verbose anymore.
Great because I can see alls concepts of my application in one sight.

But it made me realize one thing: every concept in my application is linked with one reducer.

So my state will be:

state
|_ todos
|_ users

Is it right? I'm not sure.

Maybe in a small app with few concepts but when it scales to a big app with multiple related concepts I think this is a constraint we don't want.

Use case oriented structure

Break it differently

What if we break the app along its use cases?

Instead of having todos.js and users.js we have:

usecases
|_ addTodo.js
|_ removeTodo.js
|_ loadTodos.js
|_ authenticateUser.js
|_ loadUser.js
|_logout.js

And we can group them by concept with folders:

usecases
|_ todos
  |_ addTodo.js
  |_ removeTodo.js
  |_ loadTodos.js
|_ users
  |_ authenticateUser.js
  |_ loadUser.js
  |_logout.js

An action embed its own reducer

And what's in addTodo.js?

export const addTodo = (newTodo) => {
  return {
    type: 'ADD_TODO',
    newTodo,
    perform: (state, action) => {
      return {...state, todos: [...state.todos, action.newTodo]}
    }
  }
}

As you can see it's an action that embeds its own reducer under the perform attribute.

A more tricky one ? Here's loadTodos.js, it uses redux-tunk middleware:

const loadTodosSuccess = (todos) => {
  return {
    type: 'LOAD_TODOS_SUCCESS',
    todos,
    perform: (state, action) => {
      return {...state, todos: action.todos}
    }
  }
}

export const loadTodos = () => {
  return (dispatch) => {
    doSomeRequest(...)
      .then(todos => dispatch(loadTodosSuccess(todos)))
  }
}

A single reducer

As precised here, http://redux.js.org/docs/recipes/reducers/SplittingReducerLogic.html, combineReducers() is not mandatory nor belong to the core principles of redux.
So nobody will blame us if we don't use it.

In fact, every reducer will reduce the entire state instead of a substate.

And since the actions embed their own reducers, we really have only one reducer:

const reducer = (state = someInitialState, action) => {
  if (!action.perform) return state
  const newState = action.perform(state, action)
  return newState
}

export default reducer

Pros

  • I find this approach VERY expressive since the application use cases are already visible in the file structure.
  • It is easy to do use case oriented testing, which I think is a good thing.
  • There is much less boilerplate, even if it was not the first motivation
  • It enforces the CQRS analogy: actions with embedded reducer are commands and selectors (not mentionned here) are queries

Cons

As mentionned here, http://redux.js.org/docs/recipes/ReducingBoilerplate.html#actions, actions are not serializable due to the perform attribute being a function.

So you can't take a list of serialized action and dispatch them to reproduce a scenario, since the serialized actions don't have the perform attribute.

I personnaly accomodate with that since the time travelling in redux dev tools works well.

WDYT?

Wow! You read all of that, congrats! 🎉

Have I missed an important point?

Have you tried a similar but not quite the same approach ?

Could this be adapted in order to make it more aligned with redux principles?

Should I 🔥 burn 🔥 in hell to have played with sacred concepts?

@markerikson
Copy link
Contributor

As far as the file structure goes: totally feel free to do whatever you want. Redux itself doesn't care (per the Redux FAQ entry on file/folder structures ).

For the "reducers in actions":

This is certainly possible, but as you've already seen in the docs, will break time-travel debugging. It also goes against the overall principles of Redux in that in theory many different reducers should be able to respond to the same action.

You may want to read through #155, which included discussion of "cursors" and other similar "specify the path to update"-type approaches. To quote Dan:

What Redux does not give you is write cursors. This is a core design decision made for a reason.

Redux lets you manage your state using composition. Data never lives without a reducer ("store" in current docs) that manages that data. This way, if the data is wrong, it is always traceable who changed it. It is also always possible to trace which action changed the data.

With write cursors, you have no such guarantees. Many parts of code may reference the same path via cursor and update it if they want to.

So overall... it's entirely possible to write code the way you've suggested. It's not necessarily how you're intended to use Redux. But, ultimately, if it works for you, go ahead.

FWIW, I'm actually working on a blog post that will discuss what actual limitations Redux requires and why, vs how you are intended to use Redux, vs how it's possible to use Redux. If you're interested, keep an eye on my blog at http://blog.isquaredsoftware.com . I'm hoping to have it done "soon".

@acailly
Copy link
Author

acailly commented Apr 29, 2017

Thanks @markerikson !

I did see that time travelling and hot reloading would not work as expected with this approach but I was ready to give up some of these features against simplicity and expressivity.

What I did not understand well was the reason behind the reducer composition.

This part of the FAQ gave me more insights:

And particularly the associated discussions:

Maybe the good compromise would be to break actions along use cases and to break reducers along the state structure

@theoutlander
Copy link

theoutlander commented Nov 19, 2017

@acailly what if action and reducer were in the same file? This might allow you to still keep it in the same folder. I agree with you about the structure where everything is closer to the component.

Mine is something like this:

-- components
     -- login
         -- login.jsx
         -- login.test.js
         -- login.action.js
         -- login.action.test.js
         -- login.reducer.js
         -- login.reducer.test.js
         -- login.client.js (Client API to communicate with server)
         -- login.scss

@acailly
Copy link
Author

acailly commented Nov 20, 2017

Actually I often have an action which is actionable from multiple components so I don't expect components follow the same structure than actions and reducers.
I now organize components based on UI hierarchy, actions based on usecases, reducers based on state hierarchy.
However I've seen many people doing the same as you and being satisfied. Gotta try one more time 😉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants