reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.94k stars 15.27k forks source link

Recommendations for best practices regarding action-creators, reducers, and selectors #1171

Closed bvaughn closed 8 years ago

bvaughn commented 8 years ago

My team has been using Redux for a couple of months now. Along the way I've occasionally found myself thinking about a feature and wondering "does this belong in an action-creator or a reducer?". The documentation seems a bit vague on this fact. (Or perhaps I've just missed where it's covered, in which case I apologize.) But as I've written more code and more tests I've come to have stronger opinions about where things should be and I thought it would be worth sharing and discussing with others.

So here are my thoughts.

Use selectors everywhere

This first one is not strictly related to Redux but I'll share it anyway since it's indirectly mentioned below. My team uses rackt/reselect. We typically define a file that exports selectors for a given node of our state tree (eg. MyPageSelectors). Our "smart" containers then use those selectors to parameterize our "dumb" components.

Over time we've realized that there is added benefit to using these same selectors in other places (not just in the context of reselect). For example, we use them in automated tests. We also use them in thunks returned by action-creators (more below).

So my first recommendation is- use shared selectors everywhere- even when synchronously accessing data (eg. prefer myValueSelector(state) over state.myValue). This reduces the likelihood of mistyped variables that lead to subtle undefined values, it simplifies changes to the structure of your store, etc.

Do more in action-creators and less in reducers

I think this one is very important although it may not be immediately obvious. Business logic belongs in action-creators. Reducers should be stupid and simple. In many individual cases it does not matter- but consistency is good and so it's best to consistently do this. There are a couple of reasons why:

  1. Action-creators can be asynchronous through the use of middleware like redux-thunk. Since your application will often require asynchronous updates to your store- some "business logic" will end up in your actions.
  2. Action-creators (more accurately the thunks they return) can use shared selectors because they have access to the complete state. Reducers cannot because they only have access to their node.
  3. Using redux-thunk, a single action-creator can dispatch multiple actions- which makes complicated state updates simpler and encourages better code reuse.

Imagine your state has metadata related to a list of items. Each time an item is modified, added to, or removed from the list- the metadata needs to be updated. The "business logic" for keeping the list and its metadata in sync could live in a few places:

  1. In the reducers. Each reducer (add, edit, remove) is responsible for updating the list as well as the metadata.
  2. In the views (container/component). Each view that invokes an action (add, edit, remove) it is also responsible for invoking an updateMetadata action. This approach is terrible for (hopefully) obvious reasons.
  3. In the action-creators. Each action-creator (add, edit, remove) returns a thunk that dispatches an action to update the list and then another action to updates the metadata.

Given the above choices, option 3 is solidly better. Both options 1 and 3 support clean code sharing but only option 3 supports the case where list and/or metadata updates might be asynchronous. (For example maybe it relies on a web worker.)

Write "ducks" tests that focus on Actions and Selectors

The most efficient way to tests actions, reducers, and selectors is to follow the "ducks" approach when writing tests. This means you should write one set of tests that cover a given set of actions, reducers, and selectors rather than 3 sets of tests that focus on each individually. This more accurately simulates what happens in your real application and it provides the most bang for the buck.

Breaking it down further I've found that it's useful to write tests that focus on action-creators and then verify the outcome using selectors. (Don't directly test reducers.) What matters is that a given action results in the state you expect. Verifying this outcome using your (shared) selectors is a way of covering all three in a single pass.

jayesbe commented 8 years ago

@sompylasar "business logic" and "state-updating logic" are, imo, kind of the same thing.

However to chime in with my own implementation specifics.. my Actions are primarily lookups on inputs into the Action. Actually all my Actions are pure as I have moved all my "business logic" into an application context.

as an example.. this my typical reducer

export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
    default:
      return state;
  }
}

My typical actions:

export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}

export function fooResponse( res ) {
  return {
    type: 'FOO_RESPONSE',
    data: {
        isFooing: false,
        isFooed: true,
        fooData: res.data
    }
  };
}

export function fooError( res ) {
  return {
    type: 'FOO_ERROR',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: res.error
    }
  };
}

export function fooReset( res ) {
  return {
    type: 'FOO_RESET',
    data: {
        isFooing: false,
        fooData: null,
        isFooed: false,
        fooError: null,
        toFoo: true
    }
  };
}

My business logic is defined in an object stored in the context, ie..

export default class FooBar
{
    constructor(store)
    {
        this.actions = bindActionCreators({
            ...fooActions
        }, store.dispatch);
    }

    async getFooData()
    {
        this.actions.fooRequest({
            saidToFoo: true
        });

        fetch(url)
        .then((response) => {
            this.actions.fooResponse(response);
        })
    }
}

If you see my comment above I was also struggling with the best approach.. I finally refactored and settled with passing the store into the constructor of my application object and connecting all the actions to the dispatcher at this central point. All the actions my application knows about are assigned here.

I no longer use the mapDispatchToProps() anywhere. For Redux I now only mapStateToProps when creating a connected component. If I need to trigger any actions.. I can trigger them through my application object via the context.

class SomeComponent extends React.Component {
    componentWillReceiveProps(nextProps) {
        if (nextProps.someFoo != this.props.someFoo) {
            const { app } = this.context;
            app.actions.getFooData();
        }
    }
}
SomeComponent.contextTypes = {
    app: React.PropTypes.object
};

The above component doesnt need to be redux connected. It can still dispatch actions. Of course if you need to update state within the component you would turn it into a connected component to make sure that state change is propagated.

This is how I organized my core "business logic". Since my state is maintained really on a backend serrver.. this works really well for my use case.

Where you store your "business logic" is really up to you and how it fits your use case.

sompylasar commented 8 years ago

@jayesbe The following part means you have no "business logic" in the reducers, and, moreover, the state structure has moved into the action creators which create the payload that is transferred into the store via the reducer:

    case 'FOO_REQUEST':
    case 'FOO_RESPONSE':
    case 'FOO_ERROR':
    case 'FOO_RESET':
      return {
        ...state,
        ...action.data
      }; 
export function fooRequest( res ) {
  return {
    type: 'FOO_REQUEST',
    data: {
        isFooing: true,
        toFoo: res.saidToFoo
    }
  };
}
lumiasaki commented 8 years ago

@jayesbe My actions and reducers are very similar to yours, some actions receive a plain network response object as argument, and I encapsulated the logic how to handle the response data into the action and finally return a very simple object as returned value then pass to reducer via call dispatch(). Just like what you did. The problem is if your action written in this way, your action have done almost everything and the responsibility of your reducer will be very lightweight, why we have to transfer the data to store manually if the reducer just spreads the action object simply? Redux dose it automatically for us is not a tough thing at all.

dtinth commented 8 years ago

Not necessarily. But a lot of the time, part of the business process involves updating the application's state according to the business rules, so you might have to put some business logic in there.

For an extreme case, check this out: “Synchronous RTS Engines and a Tale of Desyncs” @ForrestTheWoods https://blog.forrestthewoods.com/synchronous-rts-engines-and-a-tale-of-desyncs-9d8c3e48b2be

On Apr 5, 2016 5:54 PM, "John Babak" notifications@github.com wrote:

This is the benefit of keeping the majority of state-updating logic in the reducer.

@dtinth https://github.com/dtinth Just to clarify, by saying "state-updating logic" do you mean "business logic"?

— You are receiving this because you were mentioned. Reply to this email directly or view it on GitHub https://github.com/reactjs/redux/issues/1171#issuecomment-205754910

winstonewert commented 8 years ago

@LumiaSaki The advice to keep your reducers simple while keeping complex logic in action creators goes against the recommended way to use Redux. Redux's recommended pattern is the opposite: keep action creators simple while keeping complex logic in reducers. Of course, you are free to put all your logic in action creators anyways, but in doing so you aren't followed the Redux paradigm, even if you are using Redux.

Because of that, Redux won't automatically transfer data from actions to the store. Because that's not the way you are supposed to be using Redux. Redux isn't going to be changed to facilitate using it in a way other then it was intended. Of course, you are absolutely free to do what works for you, but don't expect Redux to change for it.

For what its worth, I produce my reducers using something like:

let {reducer, actions} = defineActions({
   fooRequest: (state, res) => ({...state, isFooing: true, toFoo: res.saidToFoo}),
   fooResponse: (state, res) => ({...state, isFooing: false, isFooed: true, fooData: res.data}),
   fooError: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: res.error})
   fooReset: (state, res) => ({...state, isFooing: false, fooData: null, isFooed: false, fooError: null, toFoo: false})
})

defineActions returns both the reducer and the action creators. This way I find it very easy to keep my update logic inside the reducer while not having to spend a lot of time writing trivial action creators.

If you insist on keeping your logic in your action creators, then you shouldn't have any trouble automating the data yourself. Your reducer is a function, and it can do whatever it wants. So your reducer could be as simple as:

function reducer(state, action) {
    if (action.data) {
        return {...state, ...action.data}
   } else {
        return state;
   }
}
dtinth commented 8 years ago

Expanding on the previous points regarding business logic, I think you can separate your business logic into two parts:

I try to put as much code into this deterministic land as possible, while keeping in mind that the reducer’s sole responsibility is to keep the application state consistent, given the incoming actions. Nothing more. Anything besides that, I would do it outside of Redux, because Redux is just a state container.

sompylasar commented 8 years ago

@dtinth Great, because the previous example in https://github.com/reactjs/redux/issues/1171#issuecomment-205782740 looks totally different to what you've written in https://github.com/reactjs/redux/issues/1171#issuecomment-205888533 -- it suggests to construct a piece of state in action creators and pass them into the reducers for them to just spread the updates (this approach looks wrong to me, and I agree with the same pointed out in https://github.com/reactjs/redux/issues/1171#issuecomment-205865840 ).

jayesbe commented 8 years ago

@winstonewert

Redux's recommended pattern is the opposite: keep action creators simple while keeping complex logic in reducers.

How can you put complex logic in the ruducers and still keep them pure ?

If I am calling fetch() for example and loading data from the server.. then processing it in some way. I have yet to see an example of a reducer that has "complex logic"

markerikson commented 8 years ago

@jayesbe : Uh... "complex" and "pure" are orthogonal. You can have really complicated conditional logic or manipulation inside a reducer, and as long as it's just a function of its inputs with no side effects, it's still pure.

gaearon commented 8 years ago

If you have complex local state (think a post editor, tree view, etc) or handle things like optimistic updates, your reducers will contain complex logic. It really depends on the app. Some have complex requests, others have complex state updates. Some have both :-)

jayesbe commented 8 years ago

@markerikson ok logic statements are one thing.. but executing specific tasks? Like say I have one action that in one case triggers three other actions, or in another case triggers two distinct and separate actions. That logic + execution of tasks doesn't sound like they should go in reducers.

My state data / model state is on the server, view state is distinct of data model but the management of that state is on the client. My data model state is simply passed down into the view.. which is what makes my reducers and actions so lean.

markerikson commented 8 years ago

@jayesbe : I don't think anyone ever said that triggering of other actions should go in a reducer. And in fact, it shouldn't. A reducer's job is simply (currentState + action) -> newState.

If you need to tie together multiple actions, you either do it in something like a thunk or saga and fire them off in sequence, or have something listening to state changes, or use a middleware to intercept an action and do additional work.

I'm kinda confused on what the discussion is at this point, to be honest.

jayesbe commented 8 years ago

@markerikson the topic seems to be about the "business logic" and where it goes. The fact of the matter is.. it all depends on the application. There are different ways to go about it. Some more complex then others. If you find a good way to solve your problem that makes things easy for you to maintain and organize. That's all that really matters. My implementation happens to be very lean for my use case even if it goes against the paradigm.

winstonewert commented 8 years ago

As you note, all that really matters is what works for you. But here's how I'd tackle the issues you asked about.

If I am calling fetch() for example and loading data from the server.. then processing it in some way. I have yet to see an example of a reducer that has "complex logic"

My reducer takes a raw response from my server and updates my state with it. That way the processing response you talk about is done in my reducer. For example, the request might be fetch JSON records for my server, which the reducer sticks in my local records cache.

k logic statements are one thing.. but executing specific tasks? Like say I have one action that in one case triggers three other actions, or in another case triggers two distinct and separate actions. That logic + execution of tasks doesn't sound like they should go in reducers.

That depends on what you are doing. Obviously, in the server fetch case one action will trigger another. That's fully within recommended Redux procedure. However, you might also be doing something like this:

function createFoobar(dispatch, state, updateRegistry) {
   dispatch(createFoobarRecord());
   if (updateRegistry) {
      dispatch(updateFoobarRegistry());
   } else {
       dispatch(makeFoobarUnregistered());
   }
   if (hasFoobarTemps(state)) {
      dispatch(dismissFoobarTemps());
   }
}

This isn't the recommended way to use Redux. The recommended Redux way is to have a single CREATE_FOOBAR action which causes all of these desired changes.

markerikson commented 8 years ago

@winstonewert :

This isn't the recommended way to use Redux. The recommended Redux way is to have a single CREATE_FOOBAR action which causes all of these desired changes.

You have a pointer to somewhere that's specified? Because when I was doing research for the FAQ page, what I came up with was "it depends", direct from Dan. See http://redux.js.org/docs/FAQ.html#actions-multiple-actions and this answer by Dan on SO.

"Business logic" is really a pretty broad term. It can cover stuff like "Has a thing happened?", "What do we do now that this happened?", "Is this valid?", and so on. Based on Redux's design, those questions can be answered in various places depending on what the situation is, although I would see "has it happened" as more of an action creator responsibility, and "what now" is almost definitely a reducer responsibility.

Overall, my take on this entire question of "business logic" is: _"it depends_". There's reasons why you might want to do request parsing in an action creator, and reasons why you might want to do it in a reducer. There's times when your reducer might simply be "take this object and slap it into my state", and other times when your reducer might be very complex conditional logic. There's times when your action creator might be very simple, and other times when it could be complex. There's times when it makes sense to dispatch multiple actions in a row to represent steps of a process, and other times when you'd only want to dispatch a generic "THING_HAPPENED" action to represent all of it.

About the only hard-and-fast rule I'd agree with is "non-determinism in action creators, pure determinism in reducers". That's a given.

Other than that? Find something that works for you. Be consistent. Know why you're doing it a certain way. Go with it.

sompylasar commented 8 years ago

although I would see "has it happened" as more of an action creator responsibility, and "what now" is almost definitely a reducer responsibility.

That's why there is a parallel discussion how to put side-effects, i.e. the non-pure part of the "what now", into reducers: #1528 and make them just pure descriptions of what should happen, like the next actions to dispatch.

cpsubrian commented 8 years ago

The pattern I've been using is:

winstonewert commented 8 years ago

From earlier in this thread, Dan's statement was:

So our official recommendation is that you should first try to have different reducers respond to the same actions. If it gets awkward, then sure, make separate action creators. But don't start with this approach.

From that, I take it that the recommended approach is to dispatch one action per event. But, pragmatically, do what works.

markerikson commented 8 years ago

@winstonewert : Dan's referring to the "reducer composition" pattern, ie, "is an action only ever listened to by one reducer" vs "many reducers can respond to the same action". Dan is very big on arbitrary reducers responding to a single action. Others prefer stuff like the "ducks" approach, where reducers and actions are VERY tightly bundled, and only one reducer ever handles a given action. So, that example's not about "dispatching multiple actions in sequence", but rather "how many portions of my reducer structure are expecting to respond to this".

But, pragmatically, do what works.

:+1:

jayesbe commented 8 years ago

@sompylasar I see the error of my ways by having the state structure in my Actions. I can easily shift the state structure into my reducers and simplify my actions. Cheers.

winstonewert commented 8 years ago

It seems to me that its the same thing.

Either you have a single action triggering multiple reducers causing multiple state changes, or you have a multiple actions each triggering a single reducer causing a single state change. Having multiple reducers respond to an action and having an event dispatch multiple actions are alternative solutions to the same problem.

In the StackOverflow question you mention, he states:

Keep action log as close to the history of user interactions as you can. However if it makes reducers tricky to implement consider splitting some actions in several, if a UI update can be thought of two separate operations that just happen to be together.

As I see it, Dan endorses maintaining one action per user interaction as the ideal way. But he's pragmatic, when it makes the reducer's tricky to implement he endorses splitting the action.

markerikson commented 8 years ago

I'm visualizing a couple similar but somewhat different use cases here:

1) An action requires updates to multiple areas of your state, particularly if you're using combineReducers to have separate reducer functions handling each sub-domain. Do you:

2) You've got a set of steps that happen in a specific sequence, each step requiring some state update or middleware action. Do you:

So yeah, definitely some overlap, but I think part of the difference in mental picture here is the various use cases.

lumiasaki commented 8 years ago

@markerikson So your advice is 'it depends on what situation you met', and how to balance 'business logic' on actions or reducers is just up to your consideration, we should also take benefits of pure function as much as possible?

markerikson commented 8 years ago

Yeah. Reducers have to be pure, as a Redux requirement (except in 0.00001% of special cases). Action creators absolutely _do not_ have to be pure, and in fact are where most of your "impurities" will live. However, since pure functions are obviously easier to understand and test than impure functions, if you can make some of your action creation logic pure, great! If not, that's fine.

And yes, from my point of view, it's up to you as a developer to determine what an appropriate balance is for your own app's logic and where it lives. There is no single hard-and-fast rule for which side of the action creator / reducer divide it should live on. (Erm, except for the "determinism / non-determinism" thing I mentioned above. Which I clearly meant to reference in this comment. Obviously.)

slorber commented 8 years ago

@cpsubrian

What To Do With Result: Reducer

Actually this is why sagas are for: to deal with effects like "if this happened, then that should also happen"


@markerikson @LumiaSaki

Action creators absolutely do not have to be pure, and in fact are where most of your "impurities" will live.

Actually action creators are not even required to be impure or to even exist. See http://stackoverflow.com/a/34623840/82609

And yes, from my point of view, it's up to you as a developer to determine what an appropriate balance is for your own app's logic and where it lives. There is no single hard-and-fast rule for which side of the action creator / reducer divide it should live on.

Yes but it's not so obvious to notice the drawbacks of each approach without experience :) See also my comment here: https://github.com/reactjs/redux/issues/1171#issuecomment-167585575

No strict rule works fine for most simple apps, but if you want to build reusable components, these components should not be aware of something outside their own scope.

So instead of defining a global action list for your whole app, you can start splitting your app into reusable components and each component has its own list of actions, and can only dispatch/reduce these. The problem is then, how do you express "when date selected in my date picker, then we should save a reminder on that todo item, show a feedback toast, and then navigate the app to the todos with reminders": this is where the saga comes to action: orchestrating the components

See also https://github.com/slorber/scalable-frontend-with-elm-or-redux

winstonewert commented 8 years ago

And yes, from my point of view, it's up to you as a developer to determine what an appropriate balance is for your own app's logic and where it lives. There is no single hard-and-fast rule for which side of the action creator / reducer divide it should live on.

Yes, there is no requirement from Redux whether you put your logic in the reducers or action creators. Redux won't break either way. There is no hard and fast rule that requires you to do it one way or the other. But Dan's recommendation was to "Keep action log as close to the history of user interactions as you can." Dispatching a single action per user event isn't required, but it is recommended.

leonardoanalista commented 8 years ago

In my case I have 2 reducers interested in 1 action. The raw action.data is not enough. They need to handle a transformed data. I didn't want to perform the transformation in the 2 reducers. So I moved the function to perform transformation to a thunk. This way my reducers receive a data ready for consumption. This is the best I could think in my short 1month redux experience.

dcoellarb commented 8 years ago

What about decoupling the components/views from the structure of the store? my goal is that anything that is affected by the structure of the store should be manage in the reducers, that is why i like to colocate selectors with reducers, so components don't really need to know how get a particular node of the store.

That's great for passing data to the components, what about the other way around, when components dispatch actions:

Let say for example in a Todo app i'm updating the name of a Todo item, so i dispatch an action passing the portion of the item i want to update i.e.:

dispatch(updateItem({name: <text variable>}));

, and the action definition is:

const updateItem = (updatedData) => {type: "UPDATE_ITEM", updatedData}

which in turn is handle by the reducer which could simply do:

Object.assign({}, item, action.updatedData)

to update the item.

This works great as i can reuse the same action and reducer to update any prop of the Todo item, ie:

updateItem({description: <text variable>})

when the description is changed instead.

But here the component needs to know how a Todo item is defined in the store and if that definition changes i need to remember to change it in all the components that depend on it, which is obviously a bad idea, any suggestions for this scenario?

winstonewert commented 8 years ago

@dcoellarb

My solution in this sort of situation is to take advantage of Javascript's flexibility to generate what would be boilerplate.

So I might have:

const {reducer, actions, selector} = makeRecord({
    name: TextField,
    description: TextField,
    completed: BooleanField
})

Where makeRecord is a function to automatically build reducers, action creators, and selectors from my description. That eliminates the boilerplate, but if I need to do something which doesn't fit this neat pattern later, I can add custom reducer/actions/selector to the result of makeRecord.

dcoellarb commented 8 years ago

tks @winstonewert i like the aproach to avoid the boilerplate, i can see that saving a lot of time in apps with a lot of models; but i still don't see how this will decouple the component from store structure, i mean even if the action is generated the component will still need to pass the updated fields to it, which means that the component still needs to know the structure of the store right?

sompylasar commented 8 years ago

@winstonewert @dcoellarb In my opinion, the action payload structure should belong to actions, not to reducers, and be explicitly translated into state structure in a reducer. It should be a lucky coincidence that these structures mirror each other for initial simplicity. These two structures don't need to always mirror each other becausr they are not the same entity.

dcoellarb commented 8 years ago

@sompylasar right, i do that, i translate the api/rest data to my store structure, but still the only one that should know the store structure is the reducers and the selectors right? reason why i colocate them, my issue is with the components/views i would prefer them to not need to know the store structure in case i decide to change it later, but as explained in my example they need to know the structure so they can send the right data to be updated, i have not find a better way to do it :(.

sompylasar commented 8 years ago

@dcoellarb You may think of your views as inputs of data of certain type (like string or number, but structured object with fields). This data object does not necessarily mirror the store structure. You put this data object into an action. This is not coupling with store structure. Both store and view have to be coupled with action payload structure.

dcoellarb commented 8 years ago

@sompylasar makes sense, i'll give it a try, thanks a lot!!!

frankandrobot commented 8 years ago

I should probably also add that you can make actions more pure by using redux-saga. However, redux-saga struggles to handle async events, so you can take take this idea a step further by using RxJS (or any FRP library) instead of redux-saga. Here's an example using KefirJS: https://github.com/awesome-editor/awesome-editor/blob/saga-demo/src/stores/app/AppSagaHandlers.js

IceOnFire commented 8 years ago

Hi @frankandrobot,

redux-saga struggles to handle async events

What do you mean by this? Isn't redux-saga made to handle async events and side-effects in an elegant way? Take a look at https://github.com/reactjs/redux/issues/1171#issuecomment-167715393

frankandrobot commented 8 years ago

No @IceOnFire . Last time I read the redux-saga docs, handling complex async workflows is hard. See, for example: http://yelouafi.github.io/redux-saga/docs/advanced/NonBlockingCalls.html
It said (still says?) something to the effect

we'll leave the rest of the details to the reader because it's starting to get complex

Compare that with the FRP way: https://github.com/frankandrobot/rflux/blob/master/doc/06-sideeffects.md#a-more-complex-workflow
That entire workflow is handled completely. (In only a few lines I might add.) On top of that, you still get most of the goodness of redux-saga (everything is a pure function, mostly easy unit tests).

The last time I thought about this, I came to the conclusion that the problem is redux-saga is makes everything look synchronous. It's great for simple workflows but for complex async workflows, it's easier if you handle the async explicitly... which is what FRP excels in.

IceOnFire commented 8 years ago

Hi @frankandrobot,

Thanks for your explanation. I don't see the quote you mentioned, maybe the library evolved (for example, I now see a cancel effect I never saw before).

If the two examples (saga and FRP) are behaving exactly the same then I don't see much difference: one is a sequence of instructions inside try/catch blocks, while the other is a chain of methods on streams. Due to my lack of experience on streams I even find more readable the saga example, and more testable since you can test every single yield one by one. But I'm quite sure this is due to my mindset more than the technologies.

Anyway I would love to know @yelouafi's opinion on this.

morgs32 commented 7 years ago

@bvaughn can you point to any decent example of testing action, reducer, selector in the same test as you describe here?

The most efficient way to tests actions, reducers, and selectors is to follow the "ducks" approach when writing tests. This means you should write one set of tests that cover a given set of actions, reducers, and selectors rather than 3 sets of tests that focus on each individually. This more accurately simulates what happens in your real application and it provides the most bang for the buck.

bvaughn commented 7 years ago

Hi @morgs32 😄

This issue is a bit old and I haven't used Redux in a while. There is a section on the Redux site about Writing Tests that you may want to check it out.

Basically I was just pointing out that- rather than writing tests for actions and reducers in isolation- it can be more efficient to write them together, like so:

import configureMockStore from 'redux-mock-store'
import { actions, selectors, reducer } from 'your-redux-module';

it('should support adding new todo items', () => {
  const mockStore = configureMockStore()
  const store = mockStore({
    todos: []
  })

  // Start with a known state
  expect(selectors.todos(store.getState())).toEqual([])

  // Dispatch an action to modify the state
  store.dispatch(actions.addTodo('foo'))

  // Verify the action & reducer worked successfully using your selector
  // This has the added benefit of testing the selector also
  expect(selectors.todos(store.getState())).toEqual(['foo'])
})

This was just my own observation after using Redux for a few months on a project. It's not an official recommendation. YMMV. 👍

koutsenko commented 7 years ago

"Do more in action-creators and less in reducers"

What if application is server&client and server must contain business logic and validators? So I send action as is, and most work will be done at server-side by reducer...

mrdulin commented 7 years ago

I have some different opinions.

My choice is fat reducer, thin action creators.

My action creators just dispatch actions(async, sync, serial-async, parallel-async, parallel-async in for-loop) based on some promise middleware .

My reducers split into many small state slices to handle the business logic. use combineReduers combine them. reducer is pure function, so it's easy to be re-used. Maybe someday I use angularJS, I think I can re-use my reducer in my service for the same business logic. If your reducer has many line codes, it maybe can split into smaller reducer or abstract some functions.

Yes, there are some cross-state cases in which the meanings A depend on B, C.. and B, C are async data. We must use B, C to fill or initialize A. So that's why I use crossSliceReducer.

About Do more in action-creators and less in reducers.

  1. If you use redux-thunk or etc. Yeah. You can access the complete state within action creators by getState(). This is a choice. Or, You can create some crossSliceReducer, so you can access the complete state too, you can use some state slice to compute your other state.

About Unit testing

reducer is pure function. So it's easy to be testing. For now, I just test my reducers, because it's most important than other parts.

To test action creators? I think if they are "fat", it may be not easy to be testing. Especially async action creators.

AlastairTaft commented 7 years ago

I agree with you @mrdulin that's now the way I've gone too.

Vanuan commented 7 years ago

@mrdulin Yeah. It looks like middleware is the right place to place your impure logic. But for business logic reducer does not seem like a right place. You'll end up with mutliple "synthetic" actions that don't represent what user has asked but what your business logic requires.

Much simpler choice is just call some pure functions/class methods from the middleware:

middleware = (...) => {
  // if(action.type == 'HIGH_LEVEL') 
  handlers[action.name]({ dispatch, params: action.payload })
}
const handlers = {
  async highLevelAction({ dispatch, params }) {
    dispatch({ loading: true });
    const data = await api.getData(params.someId);
    const processed = myPureLogic(data);
    dispatch({ loading: false, data: processed });
  }
}
timotgl commented 6 years ago

@bvaughn

This reduces the likelihood of mistyped variables that lead to subtle undefined values, it simplifies changes to the structure of your store, etc.

My case against using selectors everywhere, even for trivial pieces of state that don't require memoization or any sort of data transformation:

Obviously selectors are necessary, but I'd like to hear some other arguments for making them a mandatory API.

IceOnFire commented 6 years ago

From a practical point of view, one good reason in my experience is that libraries such as reselect or redux-saga leverage selectors to access pieces of state. This is enough for me to stick with selectors.

Philosophically speaking, I always see selectors as the "getter methods" of the functional world. And for the same reason why I would never access public attributes of a Java object, I would never access substates directly in a Redux app.

timotgl commented 6 years ago

@IceOnFire There's nothing to leverage if the computation isn't expensive or data transformation isn't required.

Getter methods might be common practice in Java but so is accessing POJOs directly in JS.

sompylasar commented 6 years ago

@timotgl

Why is there an API in-between the store and other redux code?

Selectors are a reducer's query (read) public API, actions are a reducer's command (write) public API. Reducer's structure is its implementation detail.

Selectors and actions are used in the UI layer, and in the saga layer (if you use redux-saga), not in the reducer itself.

timotgl commented 6 years ago

@sompylasar Not sure I follow your point of view here. There is no alternative to actions, I must use them to interact with redux. I don't have to use selectors however, I can just pick something directly from the state when it is exposed, which it is by design.

You're describing a way to think about selectors as a reducer's "read" API, but my question was what justifies making selectors a mandatory API in the first place (mandatory as in enforcing that as a best practice in a project, not by library design).

sompylasar commented 6 years ago

@timotgl Yes, selectors are not mandatory. But they make a good practice to prepare for future changes in the app code, making possible to refactor them separately:

When you're about to change the store structure, without selectors you'll have to find and refactor all the places where the affected pieces of state are accessed, and this could potentially be a non-trivial task, not simply find-and-replace, especially if you pass around the fragments of state obtained directly from the store, not via a selector.