Closed bvaughn closed 8 years ago
Curious if you use Immutable.js or other. In the handful of redux things I've built I couldn't imagine not using immutable, but I do have a pretty deeply nested structure that Immutable helps tame.
Wow. What an oversight for me not to mention that. Yes! We use Immutable! As you say, it's hard to imagine not using it for anything substantial.
@bvaughn One area I've struggled with is where to draw the line between Immutable and the components. Passing immutable objects into the Components lets you use pure-render decorators/mixins very easily but then you end up with IMmutable code in your components (which I don't like). So far I have just caved and done that but I suspect you use selectors in the render() methods instead of directly accessing Immutable.js' methods?
To be honest this is something we haven't defined a hard policy on yet. Often we use selectors in our "smart" containers to extra native values from our immutable objects and then pass the native values to our components as strings, booleans, etc. Occasionally we'll pass an Immutable object but when we do- we almost always pass a Record
type so that the component can treat it like a native object (with getters).
I've been moving in the opposite direction, making action creators more trivial. But I'm really just starting out with redux. Some questions about your approach:
1) How do you test your action creators? I like moving as much logic as possible to pure, synchronous functions that don't depend on external services because its easier to test. 2) Do you use the time travel with hot reloading? One of the neat things with with react redux devtools is that when hot reloading is setup, the store will rerun all of the actions against the new reducer. If I were to move my logic into the action creators, I'd lose that. 3) If your action creators dispatch multiple times to bring about an effect, does that mean that your state is briefly in an invalid state? (I'm thinking here of multiple synchronous dispatches, not ones dispatch asynchronously at a later point)
Yes that seems like saying your reducers are an implementation detail of your state and that you expose your state to your component through a query API. Like any interface it permits to decouple and make it easy to refactor the state.
IMO with new JS syntax it's not so much useful to use ImmutableJS anymore as you can easily modify lists and objects with normal JS. Unless you have very large lists and objects with lots of properties and you need structural sharing for performance reasons ImmutableJS is not a strict requirement.
@bvaughn you should really look at this project: https://github.com/yelouafi/redux-saga When I started discussing about sagas (initially backend concept) to @yelouafi it was to solve this kind of problem. In my case I first tried to use sagas while plugging in an user onboarding on an existing app.
1) How do you test your action creators? I like moving as much logic as possible to pure, synchronous functions that don't depend on external services because its easier to test.
I tried to describe this above, but basically... I think it makes the most sense (to me so far) to test your action-creators with a "ducks"-like approach. Begin a test by dispatching the result of an action-creator and then verify the state using selectors. This way- with a single test you can cover the action-creator, its reducer(s), and all related selectors.
2) Do you use the time travel with hot reloading? One of the neat things with with react redux devtools is that when hot reloading is setup, the store will rerun all of the actions against the new reducer. If I were to move my logic into the action creators, I'd lose that.
No, we don't use time-travel. But why would your business logic being in an action-creator have any impact here? The only thing that updates your application's state is your reducers. And so re-running the created actions would achieve the same result either way.
3) If your action creators dispatch multiple times to bring about an effect, does that mean that your state is briefly in an invalid state? (I'm thinking here of multiple synchronous dispatches, not ones dispatch asynchronously at a later point)
Transient invalid state is something you can't really avoid in some cases. So long as there is eventual consistency then it's usually not a problem. And again, your state could be temporarily invalid regardless of your business logic being in the action-creators or the reducers. It has more to do with side-effects and the specifics of your store.
IMO with new JS syntax it's not so much useful to use ImmutableJS anymore as you can easily modify lists and objects with normal JS. Unless you have very large lists and objects with lots of properties and you need structural sharing for performance reasons ImmutableJS is not a strict requirement.
The primary reasons for using Immutable (in my eyes) aren't performance or syntactic sugar for updates. The primary reason is that it prevents you (or someone else) from accidentally mutating your incoming state within a reducer. That's a no-no and it's unfortunately easy to do with plain JS objects.
@bvaughn you should really look at this project: https://github.com/yelouafi/redux-saga When I started discussing about sagas (initially backend concept) to @yelouafi it was to solve this kind of problem. In my case I first tried to use sagas while plugging in an user onboarding on an existing app.
I have actually checked out that project before :) Although I haven't yet used it. It does look neat.
I tried to describe this above, but basically... I think it makes the most sense (to me so far) to test your action-creators with a "ducks"-like approach. Begin a test by dispatching the result of an action-creator and then verify the state using selectors. This way- with a single test you can cover the action-creator, its reducer(s), and all related selectors.
Sorry, I got that part. What I was wondering about is the part of tests that interacts with the asynchronicity. I might write a test something like this:
var store = createStore();
store.dispatch(actions.startRequest());
store.dispatch(actions.requestResponseReceived({...});
strictEqual(isLoaded(store.getState());
But what does your test look like? Something like this?
var mock = mockFetch();
store.dispatch(actions.request());
mock.expect("/api/foo.bar").andRespond("{status: OK}");
strictEqual(isLoaded(store.getState());
No, we don't use time-travel. But why would your business logic being in an action-creator have any impact here? The only thing that updates your application's state is your reducers. And so re-running the created actions would achieve the same result either way.
What if the code is changed? If I change the reducer, the same actions are replayed but with the new reducer. Whereas if I change the action creator, that the new versions aren't replayed. So to consider two scenarios:
With a reducer:
1) I try an action in my app. 2) There is a bug in my reducer, resulting in the wrong state. 3) I fix the bug in the reducer and save 4) Time travel loads the new reducer and puts me in the state I should have been in.
Whereas with an action creator
1) I try an action in my app. 2) There is a bug in the action creator, resulting in the wrong action being created 3) I fix the bug in the action creator and save 4) I'm still in the incorrect state, which will require me to at least try the action again, and possibly refresh if it put me in a completely broken state.
Transient invalid state is something you can't really avoid in some cases. So long as there is eventual consistency then it's usually not a problem. And again, your state could be temporarily invalid regardless of your business logic being in the action-creators or the reducers. It has more to do with side-effects and the specifics of your store.
I guess my way of thinking of redux insists that the store is always in a valid state. The reducer always takes a valid state and produces a valid state. What cases do you think forces one to allow some inconsistent states?
Sorry for interruption, but what do you mean by invalid and valid states here? Data being loaded or action being executed but not yet finished look like a valid transient state to me.
What do you mean by transient state in Redux @bvaughn and @sompylasar ? Weither the dispatch finishes, or it throws. If it throws then the state do not change.
Unless your reducer has code issues, Redux only has states that are consistent with the reducer logic. Somehow all actions dispatched are handled in a transaction: weither the whole tree updates, or the state does not change at all.
If the whole tree updates but not in an appropriate way (like a state that React can't render), it's just you don't have done your job correctly :)
In Redux the current state is to consider that a single dispatch is a transaction boundary.
However I understand the concern of @winstonewert that seems to want to dispatch 2 actions synchronously in a same transaction. Because sometimes actionCreators dispatch multiple actions and expect that all the actions are executed correctly. If 2 actions are dispatched and then the second one fail, then only the 1st one will be applied, leading to a state that we could consider "inconsistent". Maybe @winstonewert wants that if the 2nd action dispatch failes, then we rollback the 2 actions.
@winstonewert I've implemented something like that in our internal framework here and it works fine until now: https://github.com/stample/atom-react/blob/master/src/atom/atom.js I also wanted to handle rendering errors: if a state can't be rendered successfully I wanted my state to be rollbacked to avoid blocking the UI. Unfortunatly until next release React does a very bad job when the render methods throw errors so it was not that much useful but may be in the future.
I'm pretty sure that we can allow a store to accept multiple sync dispatches in a transaction with a middleware.
However I'm not sure it would be possible to rollback the state in case of rendering error, as generally the redux store has already "commited" when we try to render its state. In my framework there is a "beforeTransactionCommit" hook that I use to trigger the rendering and to eventually rollback on any render error.
@gaearon I wonder if you plan to support these kind of features and if it would be possible with the current API.
It seems to me that redux-batched-subscribe does not permit to do real transaction but just reduce the number of renderings. What I see is that the store "commit" after each dispatch even if the subscription listener is only fired once at the end
Why do we need complete transaction support? I don't think I understand the use case.
@gaearon I'm not really sure yet but would be happy to know more of @winstonewert usecase.
The idea is that you could do dispatch([a1,a2])
and if a2 fails, then we rollback to the state before a1 was dispatched.
In the past I've often been dispatching multiple actions synchronously (on a single onClick listener for example, or in an actionCreator) and primarily implemented transactions as a way to call render only at the end of all actions being dispatched, but this has been solved in a different way by the redux-batched-subscribe project.
In my usecases the actions I used to fire on a transaction was mostly to avoid unnecessary renderings, but the actions did make sense independently so even if the dispatch failed for the 2nd action, not rollbacking the 1st action would still give me a consistent state (but maybe not the one that was planned...). I don't really know if someone can come up with a usecase where a full rollback would be useful
However when the rendering fails doesn't it make sense to try to rollback to the last state for which the render does not fail instead of trying to make progress on an unrenderable state?
Would a simple reducer enhancer work? e.g.
const enhanceReducerWithTheAbilityToConsumeMultipleActions = (reducer =>
(state, actions) => (typeof actions.reduce === 'function'
? actions.reduce(reducer, state)
: reducer(state, actions)
)
)
With that, you can dispatch an array to the store. The enhancer unpacks individual action and feeds it to each reducer.
Yes, and it exists: https://github.com/tshelburne/redux-batched-actions
Ohh @gaearon I did not know that. I did not notice there were 2 distinct projects that try to solve a quite similar usecase in different ways:
Both will permit to avoid unnecessary renderings, but the 1st one would rollback all the batched actions while the 2nd one would only not apply the failing action.
@gaearon Ouch, my bad for not looking at that. :flushed:
I probably haven’t had as much hands-on experience with Redux as most people, but at first sight, I have to disagree with “do more in action creators and do less in reducers,” I’ve had some similar discussion inside our company.
In Hacker Way: Rethinking Web App Development at Facebook where the Flux pattern is introduced, the very problem that leads to Flux being invented is imperative code.
In this case, an Action Creator that does I/O is that imperative code.
We’re not using Redux at work, but at where I work we used to have fine grained actions (that, of course, all make sense on its own) and trigger them in a batch. For example, when you click on a message, three actions are triggered: OPEN_MESSAGE_VIEW
, FETCH_MESSAGE
, MARK_NOTIFICATION_AS_READ
.
Then it turns out that these “low-level” actions are no more than a “command” or “setter” or “message” to set some value inside a store. We might as well go back and use MVC and end up with simpler code if we keep doing it like this.
In a sense, Action Creators represent impure code, while Reducers (and Selectors) represent pure code. Haskell people have figured out that it’s better to have less impure code and more pure code.
For instance, in my side project (using Redux), I use webkit’s speech recognition API. It emits onresult
event as you speak. There are two choices — where do these events get processed?
I went with number two: Just send the raw event object into the store.
However, it seems that Redux dev-tools doesn’t like it when non-plain objects are sent into the store, so I added some tiny logic in the action creator to transform these event objects into plain objects. (The code in the action creator is so trivial that it can’t go wrong.)
Then the reducer can combine these very primitive events and build up the transcript of what’s spoken. Because that logic lives inside pure code, I can very easily tweak it live (by hot-reloading the reducer).
I’d like to support @dtinth. Actions should represent events that happened from the real world, not how we want to react to these events. In particular, see CQRS: we want to log as much detail about a real life events, and likely the reducers will be improved in the future and process old events with new logic.
@dtinth @denis-sokolov I agree with you too on that. Btw when I was referencing the redux-saga project I maybe not made it clear that I'm against the idea of making the actionCreators grow and be more and more complex over time.
The Redux-saga project is also an attempt to do what you are describing @dtinth but there is a subtile difference with what you both say. It seems you want to say if you write every raw event that happened to the action log then you can easily compute any state from the reducers of this action log. This is absolutly true, and I've taken that path for a while until my app became very hard to maintain because the action log became not explicit, and reducers too complex over time.
Maybe you can look at this point of the original discussion that lead to Redux saga discussion: https://github.com/paldepind/functional-frontend-architecture/issues/20#issuecomment-162822909
Imagine you have a Todo app, with the obvious TodoCreated event. Then we ask you to code an app onboarding. Once the user creates a todo, we should congratulate him with a popup.
This is what @bvaughn seems to prefer
function createTodo(todo) {
return (dispatch, getState) => {
dispatch({type: "TodoCreated",payload: todo});
if ( getState().isOnboarding ) {
dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
}
}
}
I don't like this approach because it makes the action creator highly coupled to the app view's layout. It assumes the actionCreator should know the structure of the UI state tree to take its decision.
This is what @denis-sokolov @dtinth seems to prefer:
function onboardingTodoCreateCongratulationReducer(state = defaultState, action) {
var isOnboarding = isOnboardingReducer(state.isOnboarding,action);
switch (action) {
case "TodoCreated":
return {isOnboarding: isOnboarding, isCongratulationDisplayed: isOnboarding}
default:
return {isOnboarding: isOnboarding, isCongratulationDisplayed: false}
}
}
Yes you can create a reducer that knows if the congratulation should be displayed. But then you have a popup that will be displayed without even an action saying that the popup has been displayed. In my own experience doing that (and still have legacy code doing that) it is always better to make it very explicit: NEVER display the congratulation popup if no action DISPLAY_CONGRATULATION is fired. Explicit is much easier to maintain than implicit.
The redux-saga uses generators and may look a bit complicated if you are not used to but basically with a simplified implementation you would write something like:
function createTodo(todo) {
return (dispatch, getState) => {
dispatch({type: "TodoCreated",payload: todo});
}
}
function onboardingSaga(state, action, actionCreators) {
switch (action) {
case "OnboardingStarted":
return {onboarding: true, ...state};
case "OnboardingStarted":
return {onboarding: false, ...state};
case "TodoCreated":
if ( state.onboarding ) dispatch({type: "ShowOnboardingTodoCreateCongratulation"});
return state;
default:
return state;
}
}
The saga is a stateful actor that receive events and may produce effects. Here it is implemented as an impure reducer to give you an idea of what it is but it actually is not in redux-saga project.
If you take care of the initial rule it is not very explicit about everything. If you look at the above implementations, you will notice that the congratulation popup opens everytime we create a todo during the onboarding. Most likely, we want it to open only for the first created todo that happens during the onboarding and not all of them. Also, we want to allow the user to eventually redo the onboarding from the beginning.
Can you see how the code would become messy in all 3 implementations over time as the onboarding becomes more and more complicated?
With redux-saga and the above onboarding rules, you would write something like
function* onboarding() {
while ( true ) {
take(ONBOARDING_STARTED)
take(TODO_CREATED)
put(SHOW_TODO_CREATION_CONGRATULATION)
take(ONBOARDING_ENDED)
}
}
I think it solves this usecase in a much simpler way than the above solutions. If I'm wrong please give me your simpler implementation :)
You talked about impure code, and in this case there is no impurity in the Redux-saga implementation because the take/put effects are actually data. When take() is called it does not execute, it returns a descriptor of the effect to execute, and at some point an interpreter kicks in, so you don't need any mock to test the sagas. If you are a functional dev doing Haskell think Free / IO monads.
In this case it permits to:
getState
It can also provide an interpretation layer, permitting to translate raw events into more meaningful/high-level events (a bit like ELM does by wrapping events as they bubble up).
Examples:
If you want to achieve a modular app layout with ducks, it can permit to avoid coupling ducks together. The saga becomes the coupling point. The ducks just have to know of their raw events, and the saga interpret these raw events. This is far better than having duck1 dispatching directly actions of duck2 because it makes duck1 project more easy to reuse in another context. One could however argue that the coupling point could also be in actionCreators and this is what most people are doing today.
@slorber This is an excellent example! Thanks for taking the time to explain the benefits and drawbacks of each approach clearly. (I even think that should go in the docs.)
I used to explore a similar idea (which I named “worker components”). Basically, it’s a React component that doesn’t render anything (render: () => null
), but listens to events (e.g. from stores) and triggers other side-effects. That worker component is then put inside the application root component. Just another crazy way of handling complex side effects. :stuck_out_tongue:
Lots of discussion here while I was sleeping.
@winstonewert, you raise a good point about time-travel and replaying of buggy code. I think certain types of bugs/changes won't work with time-travel either way, but I think overall you're right.
@dtinth, I'm sorry, but I'm not following along with most of your comment. Some part of your action-creator/reducer "ducks" code has to be impure, in that some part of it has to fetch data. Beyond that you lost me. One of the primary purposes of my initial post was just one of pragmatism.
@winstonewert said, "I guess my way of thinking of redux insists that the store is always in a valid state." @slorber asked, "What do you mean by transient state in Redux @bvaughn and @sompylasar ? Weither the dispatch finishes, or it throws. If it throws then the state do not change."
I'm pretty sure we're thinking about different things. When I said "transient invalid state" I was referring to a use-case like the following. For example, myself and a colleague recently released redux-search. This search middleware listens for changes to collections of searchable things and then (re-)indexes them for search. If the user supplies filter text, redux-search returns the list of resource uids that match the user's text. So consider the following:
Imagine your application store contains a few searchable objects: [{id: 1, name: "Alex"}, {id: 2, name: "Brian"}, {id: 3, name: "Charles"}]
. The user has entered filter text "e" and so the search middleware contains an array of ids 1 and 3. Now imagine that user 1 (Alex) is deleted- either in response to a user-action locally or a refresh of remote data that no longer contains that user record. At the point when your reducer updates the users collection, your store will be temporarily invalid- because redux-search will reference an id that no longer exists in the collection. Once the middleware has run again it will correct the invalid state. This sort of thing can happen anytime one node of your tree is related to another node.
@slorber said, "I don't like this approach because it makes the action creator highly coupled to the app view's layout. It assumes the actionCreator should know the structure of the UI state tree to take its decision."
I don't understand what you mean by the approach coupling the action-creator "to the app view's layout". The state tree drives (or informs) the UI. That's one of the major purposes of Flux. And your action-creators and reducers are, by definition, coupled with that state (but not with the UI).
For what it's worth the example code you wrote as something I prefer is not the sort of thing I had in mind. Maybe I did a poor job of explaining myself. I think a difficulty in discussing something like this is that it typically doesn't manifest in simple or common examples. (For example standard the TODO MVC app is not complex enough for nuanced discussions like this.)
Edited for clarity on the last point.
Btw @slorber here is an example of what I had in mind. It's a bit contrived.
Let's say your state has many nodes. One of those nodes stores shared resources. (By "shared" I mean resources that cached locally and accessed by multiple pages within your application.) These shared resources have their own action-creators and reducers ("ducks"). Another node stores information for a particular application page. Your page also has its own duck.
Let's say your page needed to load the latest and greatest Thing and then allow a user to edit it. Here's an example action-creator approach that I might use for such a situation:
import { fetchThing, thingSelector } from 'resources/thing/duck'
import { showError } from 'messages/duck'
export function fetchAndProcessThing ({ params }): Object {
const { id } = params
return async ({ dispatch, getState }) => {
try {
await dispatch(fetchThing({ id }))
const thing = thingSelector(getState())
dispatch({ type: 'PROCESS_THING', thing })
} catch (err) {
dispatch(showError(`Invalid thing id="${id}".`))
}
}
}
Maybe @winstonewert wants that if the 2nd action dispatch failes, then we rollback the 2 actions.
No. I wouldn't write an action creator that dispatches two actions. I'd define a single action that did two things. The OP seems to prefer action creators that dispatch smaller actions which allows the transient invalid states I dislike.
At the point when your reducer updates the users collection, your store will be temporarily invalid- because redux-search will reference an id that no longer exists in the collection. Once the middleware has run again it will correct the invalid state. This sort of thing can happen anytime one node of your tree is related to another node.
This is actually the kind of case that bothers me. To my mind, the index would ideally be something handled entirely by the reducer or a selector. Having to dispatch extra actions to keep the search up-to-date seems a less pure use of redux.
The OP seems to prefer action creators that dispatch smaller actions which allows the transient invalid states I dislike.
Not exactly. I'd favor single actions when in regard to your action-creator's node of the state-tree. But if a single, conceptual user "action" affects multiple nodes of the state-tree then you'll need to dispatch multiple actions. You can separately invoke each action (which I think is bad) or you could have a single action-creator dispatch the actions (the redux-thunk way, which I think is better because it hides that information from your view layer).
This is actually the kind of case that bothers me. To my mind, the index would ideally be something handled entirely by the reducer or a selector. Having to dispatch extra actions to keep the search up-to-date seems a less pure use of redux.
You're not dispatching extra actions. Search is a middleware. It's automatic. But there does exist a transient state when the two nodes of your tree do not agree.
@bvaughn Oh, sorry for being such a purist!
Well, impure code has to do with data fetching and other side-effects/IO, whereas pure code can not trigger any side effect. See this table for comparison between pure and impure code.
Flux best practices says that an action should “describe a user’s action, are not setters.” Flux docs also hinted further where these actions are supposed to come from:
When new data enters the system, whether through a person interacting with the application or through a web api call, that data is packaged into an action — an object literal containing the new fields of data and a specific action type.
Basically, actions are facts/data that describes “what happened,” not what should happen. Stores can only react to these actions synchronously, predictably, and without any other side effect. All other side effects should be handled in action creators (or sagas :wink:).
I’m not saying this is the best way or better than any other way, or even a good way. But this is what I currently consider as best practice.
For example, let’s say the user wants to view the scoreboard which requires connection to a remote server. Here’s what should happen:
Assuming actions can only reach the store as a result of user’s action or server response, we can create 5 actions.
SCOREBOARD_VIEW
(as a result of user clicking the view scoreboard button)SCOREBOARD_FETCH_SUCCESS
(as a result of successful response from the server)SCOREBOARD_FETCH_FAILURE
(as a result of error response from the server)SCOREBOARD_CLOSE
(as a result of user clicking the close button)MESSAGE_BOX_CLOSE
(as a result of user clicking the close button on the message box)These 5 actions are sufficient to handle all the requirements above. You can see that the first 4 actions have nothing to do with any "duck". Every action only describes what happened in the outside world (user wants to do this, server said that) and can be consumed by any reducer. We also don’t have MESSAGE_BOX_OPEN
action, because that’s not “what happened” (although that’s what should happen).
The only way to change the state tree is to emit an action, an object describing what happened. —Redux’s README
They are glued together with these action creators:
function viewScoreboard () {
return async function (dispatch) {
dispatch({ type: 'SCOREBOARD_VIEW' })
try {
const result = fetchScoreboardFromServer()
dispatch({ type: 'SCOREBOARD_FETCH_SUCCESS', result })
} catch (e) {
dispatch({ type: 'SCOREBOARD_FETCH_FAILURE', error: String(e) })
}
}
}
function closeScoreboard () {
return { type: 'SCOREBOARD_CLOSE' }
}
Then each part of the store (governed by reducers) can then react to these actions:
Part of Store/Reducer | Behavior |
---|---|
scoreboardView | Update visibility to true on SCOREBOARD_VIEW , false on SCOREBOARD_CLOSE and SCOREBOARD_FETCH_FAILURE |
scoreboardLoadingIndicator | Update visibility to true on SCOREBOARD_VIEW , false on SCOREBOARD_FETCH_* |
scoreboardData | Update data inside store on SCOREBOARD_FETCH_SUCCESS |
messageBox | Update visibility to true and store message on SCOREBOARD_FETCH_FAILURE , and update visibility to false on MESSAGE_BOX_CLOSE |
As you can see, a single action can affect many parts of the store. Stores are only given high-level description of an action (what happened?) rather than a command (what to do?). As a result:
It’s easier to pinpoint errors.
Nothing can affect the state of the message box. No one can tell it to open for any reason. It only reacts to what it’s subscribed to (user actions and server responses).
For example, if the server fails to fetch the scoreboard, and a message box did not appear, you do not need to find out why SHOW_MESSAGE_BOX
action is not dispatched. It becomes obvious that the message box did not handle the SCOREBOARD_FETCH_FAILURE
action properly.
A fix is trivial and can be hot-reloaded and time-traveled.
Action creators and reducers can be tested separately.
You can test whether action creators described what happens in the outside world correctly, without any regard on how stores react to them.
In the same way, reducers can simply be tested whether it reacts properly to the actions from the outside world.
(An integration test would still be very useful.)
No worries. :) I appreciate the further clarification. It actually sounds like we're agreeing here. Looking at your example action-creator, viewScoreboard
, it looks a lot like my example action-creator fetchAndProcessThing
, right above it.
Action creators and reducers can be tested separately.
While I agree with this, I think that it often makes more pragmatic sense to test them together. It's likely that either your action or or your reducer (maybe both) are super simple and so the return-on-effort value of testing the simple one in isolation is kind of low. That's why I proposed testing the action-creator, reducer, and related selectors together (as a "duck").
But if a single, conceptual user "action" affects multiple nodes of the state-tree then you'll need to dispatch multiple actions.
That's precisely where I think what you are doing differs from what is considered best practices for redux. I think the standard way is to have one action which multiple nodes of the state tree respond to.
Ah, interesting observation @winstonewert. We've been following a pattern of using unique type-constants for each "ducks" bundle and so by extension, a reducer only response to actions dispatched by its sibling action-creators. I'm honestly not sure how I feel, initially, about arbitrary reducers responding to an action. It feels like a little like bad encapsulation.
We've been following a pattern of using unique type-constants for each "ducks" bundle
Note we don't endorse it anywhere in the docs ;-) Not saying it's bad, but it gives people certain sometimes-wrong ideas about Redux.
so by extension, a reducer only response to actions dispatched by its sibling action-creators
There's no such thing as reducer / action creator pairing in Redux. That's purely a Ducks thing. Some people like it but it obscures the fundamental strengths of Redux/Flux model: state mutations are decoupled from each other and from the code causing them.
I'm honestly not sure how I feel, initially, about arbitrary reducers responding to an action. It feels like a little like bad encapsulation.
Depending on what you consider encapsulation boundaries. Actions are global in the app, and I think that's fine. One part of the app might want to react to another part's actions because of complex product requirements, and we think this is fine. The coupling is minimal: all you depend on is a string and the action object shape. The benefit is it's easy to introduce new derivations of the actions in different parts of the app without creating tons of wiring with action creators. Your components stay ignorant of what exactly happens when an action is dispatched—this is decided on the reducer end.
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.
We do recommend using selectors—in fact, we recommend exporting keeping functions that read from the state ("selectors") alongside reducers, and always using them in mapStateToProps
instead of hardcoding state structure in the component. This way it's easy to change the internal state shape. You can (but don't have to) use reselect for performance, but you can also implement selectors naïvely like in the shopping-cart
example.
Perhaps, it boils down to whether you program in imperative style or reactive style. Using ducks can cause actions and reducers to become highly-coupled, which encourages more imperative actions.
SHOW_MESSAGE_BOX
or SHOW_ERROR
DATA_FETCHING_FAILED
or USER_ENTERED_INVALID_THING_ID
. The store reacts accordingly.In previous example, I don’t have SHOW_MESSAGE_BOX
action or showError('Invalid thing id="'+id+'"')
action creator, because that’s not fact. That’s a command.
Once that fact enters the store, you can translate that fact into commands, inside your pure reducers, e.g.
// type Command = State → State
// :: Action → Command
function interpretAction (action) {
switch (action.type) {
case 'DATA_FETCHING_FAILED':
return showErrorMessage('Data fetching failed')
break
case 'USER_ENTERED_INVALID_THING_ID':
return showErrorMessage('User entered invalid thing ID')
break
case 'CLOSE_ERROR_MESSAGE':
return hideErrorMessage()
break
default:
return doNothing()
}
}
// :: (State, Action) → State
function errorMessageReducer (state, action) {
return interpretAction(action)(state)
}
const showErrorMessage = message => state => ({ visible: true, message })
const hideErrorMessage = () => state => ({ visible: false })
const doNothing = () => state => state
When an action goes into the store as “a fact” rather than “a command,” there’s less chance it can go wrong, because, well, it’s a fact.
Now, if your reducers misinterpreted that fact, it can be fixed easily and the fix can travel through time. If your action creators misinterpret that fact, however, you need to re-run your action creators.
You can also change your reducer so that when USER_ENTERED_INVALID_THING_ID
fires, the thing ID text field is reset. And that change also travels through time. You can also localize your error message and without refreshing the page. That tightens the feedback loop, and makes debugging and tweaking a lot easier.
(I am just talking about the pros here, of course there are cons. You have to think a lot more about how to represent that fact, given that your store can only respond to these facts synchronously and without side-effects. See discussion about alternatives async/side effect models and this question I posted on StackOverflow. I guess we haven’t nailed that part yet.)
I'm honestly not sure how I feel, initially, about arbitrary reducers responding to an action. It feels like a little like bad encapsulation.
It’s also very common for multiple components to take data from the same store. It’s also quite common for a single component to depend on data from multiple parts of the store. Doesn’t that too sound a little like bad encapsulation? To become truly modular, shouldn’t a React component also be inside the "duck" bundle? (Elm architecture does that.)
React makes your UI reactive (hence its name) by treating data from your store as a fact. So you don’t have to tell your view ‘how to update the UI.’
In the same way, I also believe Redux/Flux makes your data model reactive, by treating actions as a fact, so you don’t have to tell your data model how to update themselves.
Thanks for taking the time to write up and share your thoughts, @dtinth. Also thanks @gaearon for weighing in on this discussion. (I know you have a ton of stuff going on.) You've both given me some additional things to consider. :)
It’s also very common for multiple components to take data from the same store. It’s also quite common for a single component to depend on data from multiple parts of the store. Doesn’t that too sound a little like bad encapsulation?
Eh... some of this is subjective, but no. I think of the exported action-creators and selectors as the API for the module.
Anyway, I think this has been a good discussion. Like Thai mentioned in his previous response, there are pros and cons to these approaches we're discussing. It's been nice to get insight into others approaches. :)
By the way message box is a good example of where I'd prefer to have a separate action creator for showing. Mostly because I'll want to pass a time when it was created so it can be dismissed automatically (and action creator is where you call impure Date.now()
), because I want to set up a timer to dismiss it, I want to debounce that timer, etc. So I'd consider a message box the case where its "action flow" is important enough to warrant its personal actions. That said perhaps what I described can be solved more elegantly by https://github.com/yelouafi/redux-saga.
I wrote this in the Discord Reactiflux chat initially, but was asked to paste it here.
I've been thinking about the same stuff a lot recently. I feel like state updates are divided to three parts.
- The action creator is passed the minimum amount of information needed to execute the update. I.e. anything that can be computed from the current state should not be in it.
- The state is queried for any information you need to fulfill the updates (e.g. when you want to copy Todo with id X, you fetch the attributes of Todo with id X so you can make a copy). This can be done in the action creator, and that info is then included in the action object. This results in fat action objects. OR it could be calculated in the reducer - thin action objects.
- Based on that information, pure reducer logic is applied to get the next state.
Now, the problem is what to put in the action creator and what in the reducer, the choice between fat and thin action objects. If you put all the logic in the action creator, you end up with fat action objects that basically declare the updates to the state. Reducers become pure, dumb, add-this, remove that, update these functions. They will be easy to compose. But not much of your business logic will be there.
If you put more logic in the reducer, you end up with nice, thin action objects, most of your data logic in one place, but your reducers are harder to compose since you might need info from other branches. You end up with large reducers or reducers that take additional arguments from higher up in the state.
Don't know what the answer to these problems is, not sure if there is one yet
Thanks for sharing these thoughts @tommikaikkonen. I'm still undecided myself about what the "answer" is here. I agree with your summary. I'd add one small note to the "put all the logic in the action creator..." section, which is that it enables you to use (shared) selectors for reading data which in some cases can be nice.
This is an interesting thread! Figuring out where to place the code in a redux app is a problem I guess we all face. I do like the CQRS idea of just recording things that has happened. But I can see a mismatch of ideas here becuase in CQRS, AFAIK the best practice is to build a de-normlized state from the action/events that is directly consumable by the views. But in redux the best practice is to build a fully normalized state and derieve your views' data through selectors. If we build denormalized state that is directly consumable by the view, then I think the problem that a reducer wants data in another reducer goes away (becuase each reducer can just store all data it needs not caring about normalization). But then we get other problems when updating data. Maybe this is the core of the discussion?
Coming from many many years of Object Oriented development.. Redux feels like a major step backwards. I find my self begging to create classes that encapsulate events (action creators), and the business logic. I'm still trying to figure out a meaningful compromise but as of yet have been unable to do so. Does anyone else feel the same way?
Object oriented programming encourages putting reads together with writes. This makes a bunch of things problematic: snapshotting and rollback, centralized logging, debugging wrong state mutations, granular efficient updates. If you don’t feel that those are the problems to you, if you know how to avoid them while writing traditional object oriented MVC code, and if Redux introduces more problems than it solves in your app, don’t use Redux :wink: .
@jayesbe Coming from an object-oriented programming background, you may find that it clashes with emerging ideas in programming. This is one of many articles on the subject: https://www.leaseweb.com/labs/2015/08/object-oriented-programming-is-exceptionally-bad/.
By separating actions from data transformation, testing of application of business rules is simpler. Transforms become less dependent on application context.
That doesn't mean abandoning objects or classes in Javascript. For example, React components are implemented as objects, and now classes. But React components are designed simply to create a projection of supplied data. You are encouraged to use pure components that do not store state.
That is where Redux comes in: to organise application state and bring together actions and the corresponding transformation of application state.
@johnsoftek thanks for the link. However based on my experience in the last 10 years.. I don't agree with it but we don't need to get into the debate between OO and non-OO here. The issue I have is with organizing code and abstraction.
My goal is to create a single app / single architecture that can (using configuration values alone) be used to create 100's of apps. The use case I have to deal with is handling a white-label software solution that is in use by many clients.. each calling the application their own.
I have come up with what I feel is an interesting compromise.. and I think it handles it nicely enough but it may not meet the functional programming crowds standards. I'd still like to put it out there.
I have a single Application class that is self-contained with all the business logic, API wrappers, etc.. that I need to interface with my server side application.
example..
export default Application {
constructor(config) {
this.config = config;
}
config() {
return this.config;
}
login(data, cb) {
const url = [
this.config.url,
'?client=' + this.config.client,
'&username=' + data.username,
....
].join('');
fetch(url).then((responseText) => {
cb(responseText);
})
}
... more business logic
}
I created a single instance of this object and placed it into the context.. by extending the Redux Provider
import { Provider } from 'react-redux';
export default class MyProvider extends Provider {
getChildContext() {
return Object.assign({}, Provider.prototype.getChildContext.call(this), {
app: this.props.app
});
}
render() {
return this.props.children;
}
}
MyProvider.childContextTypes = {
store: React.PropTypes.object,
app: React.PropTypes.object
}
Then I used this provider as such
import Application from './application';
import config from './config';
class MyApp extends Component {
render() {
return (
<MyProvider store={store} app={new Application(config)}>
<Router />
</MyProvider>
);
}
}
AppRegistry.registerComponent('MyApp', () => MyApp);
finally in my Component I used my app..
class Login extends React.Component {
render() {
const { app } = this.context;
const { state, actions } = this.props;
return (
<View style={style.transparentContainer}>
<Form ref="form" type={User} options={options} />
<Button
onPress={() => {
value = this.refs.form.getValue();
if (value) {
app.login(value, actions.login);
}
}}
>
Login
</Button>
</View>
);
}
};
Login.contextTypes = {
app: React.PropTypes.object,
};
function mapStateToProps(state) {
return {
state: state.default.auth
};
};
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators(authActions, dispatch),
dispatch
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Login);
The Action Creator is thus just a callback function to my business logic.
app.login(value, actions.login);
This solution seems to be working nicely at the moment though I've only started with authentication.
I believe I could also pass the store into my Application instance but I don't want to do that because I don't want the store to undergo chance mutation. Though accessing the store may come in handy. I'll think about that more if I need to.
I have come up with what I feel is an interesting compromise.. and I think it handles it nicely enough but it may not meet the functional programming crowds standards.
You won’t find “the functional crowd” here :wink: . The reason we choose functional solutions in Redux is not because we’re dogmatic but because they solve some problems people often make because of classes. For example, separating reducers from action creators lets us separate reads and writes which is important for logging and reproducing bugs. Actions being plain objects make record and replay possible because they are serializable. Similarly, state being plain object rather than an instance of MyAppState
makes it very easy to serialize it on the server and deserialize it on the client for server rendering, or persist parts of it in localStorage
. Expressing reducers as functions allows us to implement time travel and hot reloading, and expressing selectors as functions makes memoization easy to add. All of these benefits have nothing to do with us being a “functional crowd” and everything to do with solving specific tasks this library was created to solve.
I created a single instance of this object and placed it into the context.. by extending the Redux Provider
This looks totally sensible to me. We don’t have an irrational hate of classes. The point is that we’d rather not use them in cases where they are severly limiting (such as for reducers or action objects), but it’s fine to use them to generate action objects, for example.
I would however avoid extending Provider
as this is fragile. There is no need for it: React merges context of components, so you can just wrap it instead.
import { Component } from 'react';
import { Provider } from 'react-redux';
export default class MyProvider extends Component {
getChildContext() {
return {
app: this.props.app
};
}
render() {
return (
<Provider store={this.props.store}>
{this.props.children}
</Provider>
);
}
}
MyProvider.childContextTypes = {
app: React.PropTypes.object
}
MyProvider.propTypes = {
app: React.PropTypes.object,
store: React.PropTypes.object
}
It actually reads easier in my view, and is less fragile.
So, all in all, your approach makes total sense. Using a class in this case is not really different from something like createActions(config)
which is a pattern we also recommend if you need to parametrize the action creators. There’s absolutely nothing wrong with it.
We only discourage you from using class instances for state and action objects because class instances make serialization very tricky. For reducers, we also don’t recommend using classes because it will be harder to use reducer composition, that is, reducers that call other reducers. For everything else, you can use any means of code organization, including classes.
If your application and configuration are immutable (and I think they should be, but perhaps I've drunk too much functional cool-aid), then you could consider the following approach:
const appSelector = createSelector(
(state) => state.config,
(config) => new Application(config)
)
And then in mapStateToProps:
function mapStateToProps(state) {
return {
app: appSelector(state)
};
};
Then you don't need the provider technique you've adopted, you just obtain the application object from the state. Thanks to reselect, the Application object will only be constructed when the config changes, which is probably just once.
Where I think this approach may have an advantage is that it easily lets you extend the idea to having multiple such objects and also having those objects depend on other parts of the state. For example, you could have a UserControl class with login/logout/etc methods that has access both to the configuration and part of your state.
So, all in all, your approach makes total sense.
:+1: Thanks that helps. I agree with the improvement on MyProvider. I'll update my code to follow. One of the biggest problems I had when first learning Redux was the semantic notion of "Action Creators" .. it didn't jive until I equated them with Events. For me it was kind of realization that was like.. these are events that are being dispatched.
@winstonewert is createSelector available on react-native ? I don't believe it is. At the same time, it does look as though you're creating the new Application every time you attach it in mapStateToProps at some component ? My objective is to have a single object instantiated that provides for all business logic to the application and for that object to be accessible globally. I'm not sure if you're suggestion works. Though I like the idea of having additional objects available if needed.. technically I can instantiate as necessary through the Application instance as well.
One of the biggest problems I had when first learning Redux was the semantic notion of "Action Creators" .. it didn't jive until I equated them with Events. For me it was kind of realization that was like.. these are events that are being dispatched.
I would say that there is no semantic notion of Action Creators in Redux at all. There is a semantic notion of Actions (which describe what happened and are roughly equivalent to events but not quite—e.g. see discussion in #351). Action creators are just a pattern for organizing the code. It’s convenient to have factories for actions because you want to make sure that actions of the same type have consistent structure, have the same side effect before they are dispatched, etc. But from Redux point of view, action creators don’t exist—Redux only sees actions.
is createSelector available on react-native ?
It is available in Reselect which is plain JavaScript without dependencies and can work on the web, on the native, on the server, etc.
Ahh. ok got it. Things are a lot more clear. Cheers.
I recently ran into an issue where nested objects were lost when Redux merges mapStateToProps and mapDispatchToProps (see reactjs/react-redux#324). Although @gaearon provides a solution that allows me to use nested objects, he goes on to say that this is an anti-pattern:
Note that grouping objects like this will cause unnecessary allocations and will also make performance optimizations harder because we can no longer rely on shallow equality of result props as a way to tell whether they changed. So you will see more renders than with a simple approach without namespacing which we recommend in the docs.
@bvaughn said
reducers should be stupid and simple
and we should put most business logic into action creators, I'm absolutely agree with that. But if everything has moved into actions, why we still have to create reducer files and functions manually? Why not put the data which has been operated in actions into store directly?
It has confused me a period of time...
why we still have to create reducer files and functions manually?
Because reducers are pure functions, if there is an error in state-updating logic, you can hot-reload the reducer. The dev tool can then rewind the app to its initial state, and then replay all the actions, using the new reducer. This means you can fix state-updating bugs without having to manually roll-back and re-perform the action. This is the benefit of keeping the majority of state-updating logic in the reducer.
This is the benefit of keeping the majority of state-updating logic in the reducer.
@dtinth Just to clarify, by saying "state-updating logic" do you mean "business logic"?
I'm not so sure putting most of your logic in action creators is a good idea. If all your reducers are just trivial functions accepting ADD_X
-like actions, then they're unlikely to have errors - great! But then all your errors have been pushed to your action creators and you lose the great debugging experience that @dtinth alludes to.
But also like @tommikaikkonen mentioned, it's not so simple writing complex reducers. My gut feeling is that is where you would want to push if you wanted to reap the benefits of Redux though - otherwise instead of pushing side effects to the edge, you're pushing your pure functions to handle only the most trivial tasks, leaving most of your app in state-ridden hell. :)
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)
overstate.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:
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:
updateMetadata
action. This approach is terrible for (hopefully) obvious reasons.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.