I have been following the widely given advice of learning React development by first mastering component props, encapsulating UI state in component level this.state and passing it down selectively through the component tree. It's been an enlightening experience. I've come to appreciate the power of the stateless view design pattern and I feel that I've been able to achieve robust and well organized results using these techniques.
Moving on, I am now trying to incorporate more sophisticated state management using redux. But as I wade through the complexity and integrate redux into my apps, I find myself confronting the following observations about how my code has evolved. Some of these developments seem sensible, but others make me question whether I'm doing things 'right'.
1) Action Creators as the nexus of business and UI logic
I find that much of the logic that was previously implemented in the React lifecycle functions componentDidUpdate etc., and in onTouch/onPress handlers, is now implemented in action creators. This seems to be a positive development as it keeps 'everything in the same place' and allows for unit testing.
Question: Is it best practice to concentrate business logic in a web of fairly intricate action creators?
2) Hollowed out reducers
As a corollary to #1 above, I find that my reducers and their corresponding action objects have evolved into a de-facto list of setters that do little more than update the state store with the passed along values, in this fashion:
case types.SAVE_ORDER:
return Object.assign({}, state, {
order: action.order,
});
A big part of the reason for this is that reducers are supposed to be pure functions and therefore I'm limited in what I can do with them (e.g. no async processing). Additionally, reducers are allowed only to operate on their respective sub-section of the store state. Given that much of my app's complexity already necessarily resides in the action creators, I find it hard to justify arbitrarily migrating complexity into reducers simply for the sake of making them 'look useful'.
Question: Is it normal, and acceptable practice to have boilerplate reducers that function merely as glorified setters to the redux store state?
3) redux-thunk everywhere
I've asked separately on SO why redux-thunk is even necessary (as opposed to calling standard action creators inside of async callbacks/utility functions). I've been pointed to this answer by Dan Abramov which provides a very satisfactory explanation (vis-a-vis scalability, server side rendering and myraid other reasons).
Having accepted the necessity of redux-thunk, I find that the majority of my action creators need to perform async actions, need access to getState, or dispatch multiple changes to the state. As a result I've been returning 'thunks' extensively.
Question: Is it normal for a redux application to rely extensively on thunk'ed action creators, and rarely to fire a standard object action directly?
4) Redux as global this.state
In the final analysis, it seems my app's redux store has evolved to effectively resemble a global this.state. You could think of it as keeping the entire application state in this.state in the outermost container component, but without the inevitable mess that comes with passing the said state down through nested layers of props, and any changes back up the component tree through a rats-nest of handler functions.
Question: Is redux the correct tool to use for a global state store? Are there alternatives out there that behave more akin to react's built-in this.state, allowing a global application state to be propagated through stateless react components, and updated from throughout the application via a centralized 'switchboard', without the seemingly endless web of boilerplate, constants and switch statements that come with adopting redux?
5) One single action type?
This follow up question is inspired by one of the posted comments.
Question: Could one legitimately (in all seriousness, not just blatantly demonstrating a point) use redux with precisely one action type?
Example - Action creator:
export function someActionCreator(various_params){
return (dispatch, getState => {
// ... business logic here ....
asyncIfThisIfThat().then(val => {
dispatch({
// type: 'UPDATE_STATE', // Don't even bother setting a type
order: val
})
})
)
}
The one universal reducer case:
export default function app(state = initialState, action = {}) {
return Object.assign({}, state, action)
// Just unconditionally merge into state!
}
Seems to me this would provide a globally scoped state object that is automatically mapped to connected components, and one that benefits from all the advantages of immutable state and interoperable with React props. In this scheme, dispatch effectively becomes a global setState.
Note - Please don't take this question wrong - this is certainly not criticism of redux. As a learner I am obviously in no position to judge a technology backed by the expertise of thousands and the support of millions. I have no doubt of its value in the right context.
I'm just sensing the smell of a questionable pattern in my own code and wondering what, if anything I'm doing wrong, or whether I'm using the right tool for the task.
My answer is mostly speaking from my own experience learning redux and using it professionally. The team I was on went down the same path of setter-like actions, and then shifted away to action names that were more event-based and describe what had happened rather than what should happen.
Question: Is it best practice to concentrate business logic in a web of fairly intricate action creators?
This depends on how your actions are named. In your case, your actions are very glorified setters, so all of your business logic is going to live inside of Action Creators. If you name your actions to be more event-like (descriptive about what happened) rather than setters, you're going to have some of the business logic shift into the reducers, and complexity removed from action creators, because event actions naturally feel more re-usable across different reducers. When you do setter actions, the tendency is to have setter-actions that interact with only 1 reducer, and create more setter-actions when you want other reducers to be involved.
If you have an app for a school, and a student is expelled, you'll likely dispatch a REMOVE_STUDENT and then aDELETE_GRADES_FOR_STUDENT action. If your action has an event-like name, you may be more inclined to just have a STUDENT_EXPELLED action that the grades reducer and student roster reducer both act upon it.
There is nothing technically stopping you from having setter-like names, and acting on them in multiple reducers, though. It's just not the tendency that my team fell into when working in Redux and using setter-like names. We didn't want to muddy up the expectations and purity that came from having concise action names, where the impact on state was very clear. REMOVE_STUDENT_GRADES and DELETE_STUDENT_FROM_ROSTER felt good.
Question: Is it normal, and acceptable practice to have boilerplate reducers that function merely as glorified setters to the redux store state?
It is normal, but not necessarily correct. This is how our codebase grew initially - we even had standards to name our actions as RESET_..., SET_..., REMOVE_..., ADD_..., UPDATE... etc. This seemed to work for a while, until we bumped into cases where we needed multiple reducers to update according to a single action.
You actions will evolve in one of these 2 ways (or both)
Dispatch multiple actions in a row (you must use a library like redux-batch-actions if you want to dispatch multiple actions in a row). We chose not to use this, because it's cumbersome and didn't feel like it scaled very well as our codebase grew in size.
Rename your actions to be more generic and re-usable across different reducers. This is what we ended up doing. Having actions as setters and getters was cumbersome. Dan Abramov and others has expressed their opinion that Redux Actions should be events (a description of a thing that has happened), rather than instructions (a description of a thing that should happen). The team I work on agreed with this, and we've moved away from the setters-style of actions. There was much debate about this earlier on when Redux was new.
In scenario 1, you might do something like this:
// student has been expelled from school, remove all of their data
store.dispatch(batchActions(
removeStudentFromClass(student),
removeStudentsGrades(student)
));
// student roster reducer
case REMOVE_STUDENT_FROM_CALLS:
/* ... */
// student grades reducer
case REMOVE_STUDENT_GRADES:
/* ... */
If you go down this path without using Batch Actions, it's an absolute nightmare. Each dispatched event will recompute state, and re-render your app. This falls apart very quickly.
// student has been expelled from school, remove all of their data
store.dispatch(removeStudentFromClass(student));
// app re-rendered, students grades exist, but no student!
store.dispatch(removeStudentsGrades(student));
In the above code, you dispatch an action to remove the student from class, and then the app re-renders. If you have a grades page open, and you can see the students grades, but the student is removed, you're very likely going to reference state in the student roster reducer to grab the student info and that can/will throw a JS error. Bad news. You have the grades for a student of undefined?! I ran into issues like this myself, and it was part of our motivation for moving to option 2 below. You'll hear about these kinds of states called "intermediate states" and they're problematic.
In scenario 2 your code might look more like this:
store.dispatch(expelStudent(student));
// student roster reducer
case EXPEL_STUDENT:
/* ... */
// student grades reducer
case EXPEL_STUDENT:
/* ... */
With the code above, the student is expelled via the action, and their data is removed from all reducers in 1 step. This scales nicely and your codebase reflects the business terms related to your app that you would talk about day-to-day. You can also perform the same state updates for multiple events if it makes sense from a business logic perspective:
case EXPEL_STUDENT:
case STUDENT_DROPPED_OUT:
case STUDENT_TRANSFERRED:
/* remove student info, all actions must have action.payload.student */
Question: Is it normal for a redux application to rely extensively on thunk'ed action creators, and rarely to fire a standard object action directly?
Yes definitely. As soon as you need to grab a little piece of data from the store in an action creator, it has to become a thunk. Thunks are very common, and should have been part of the redux library.
As our thunks grew in complexity, they got confusing and difficult to easily understand. We started to abuse promises and return values and it was taxing. Testing them was also a nightmare. You have to mock out everything, it's painful.
To solve this problem, we pulled in redux-saga. Redux-saga is easily testable with libraries like redux-saga-test-plan or redux-saga-test-engine (we use test-engine and have contributed to it as needed).
We aren't 100% sagas, and don't aim to be. We still use thunks as needed. If you need to upgrade your action to be a little smarter, and the code is very simple, there's no reason why you shouldn't upgrade that action to a thunk.
As soon as an action creator gets complex enough to warrant some unit testing, redux-saga might come in handy.
Redux-saga does have a rough learning curve to it, and feels quite bizarre at first. Testing sagas manually is miserable. Great learning experience, but I would not ever do it again.
Question: Is redux the correct tool to use for a global state store? Are there alternatives out there that behave more akin to react's built-in this.state, allowing a global application state to be propagated through stateless react components, and updated from throughout the application via a centralized 'switchboard', without the seemingly endless web of boilerplate, constants and switch statements that come with adopting redux?
MobX - I've heard good things about it from people who have your same complaints about Redux (too much boilerplate, too many files, everything is disconnected) I don't use it myself and have not used it, though. There's a good chance that you'll enjoy it more than Redux. It solves the same problem, so if you actually enjoy it more then it may be worth the switch. Developer experience is very important if you're going to work on code for a long time.
I'm okay with the Redux boilerplate and whatnot. The team I worked on has made macros to scaffold out the boilerplate of creating new actions, and we have lots of tests in place so our Redux code is pretty solid. Once you work with it for a while, you internalize the boilerplate and it's not as exhausting.
If you do stick with Redux long term, and are savvy enough to adopt flow on top of redux it's a huge win for long-term maintainability. Fully-typed redux code is amazing to work in, especially for refactoring. It's so easy to refactor a reducer/actionCreators, but forget to update unit test code. If your unit tests are covered by flow, it's going to complain that you're calling a function incorrectly immediately. It's wonderful.
Introducing Flow is a complex hurdle to get over, but well worth it. I wasn't involved in the initial set up, and I assume it's gotten easier to introduce to a codebase, but I'd imagine that it will take some learning and hours. Worth it though. Definitely 100% worth it.
Question: Could one legitimately (in all seriousness, not just blatantly demonstrating a point) use redux with precisely one reducer?
You definitely could, it could work for a small app. It wouldn't scale well for a larger team, and refactoring seems like it would become a nightmare. Splitting the store up into separate reducers lets you isolate responsibilities and concerns.
I'm a Redux maintainer. I'll give you some initial answers and point you to some learning resources, and I can answer further questions as needed.
First, Cory Danielson has given some excellent advice, and I want to echo pretty much everything he said.
Action creators, reducers, and business logic:
I'll quote the Redux FAQ entry on splitting business logic between action creators and reducers:
There's no single clear answer to exactly what pieces of logic should go in a reducer or an action creator. Some developers prefer to have “fat” action creators, with “thin” reducers that simply take the data in an action and blindly merge it into the corresponding state. Others try to emphasize keeping actions as small as possible, and minimize the usage of getState() in an action creator. (For purposes of this question, other async approaches such as sagas and observables fall in the "action creator" category.)
There are some potential benefits from putting more logic into your reducers. It's likely that the action types would be more semantic and more meaningful (such as "USER_UPDATED" instead of "SET_STATE"). In addition, having more logic in reducers means that more functionality will be affected by time travel debugging.
This comment sums up the dichotomy nicely:
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.
I talked about this topic some more in my post The Tao of Redux, Part 2 - Practice and Philosophy earlier this year (specifically the sections on action semantics and thick vs thin reducers, and again in a recent Reddit comment thread.
Use of thunks:
Yes, thunks are a valuable tool for any complex synchronous logic that needs to live outside a component, including any code that needs access to the current store state. They're also useful for simple async logic (like basic AJAX calls with just success/failure handlers). I discussed the pros and cons of thunks in my post Idiomatic Redux: Thoughts on Thunks, Sagas, Abstraction, and Reusability.
In my own app, I use thunks in many places, as well as sagas for more complex async logic and workflows, and highly recommend thunks as a useful tool overall.
Redux as a global store
I've frequently said that you can use as much or as little abstraction on top of Redux as you want. You don't have to use switch statements, you can use lookup tables or any other conditional logic in your reducers, and you are highly encouraged to reuse reducer logic. In fact, there's dozens of existing utilities to generate reusable action creators and reducers, as well as many higher-level abstraction libraries written on top of Redux.
Use of a single blind-setter reducer
This is another topic I looked at in The Tao of Redux, Part 2, and someone else had a good comment another recent Reddit thread. It's certainly technically feasible to do so, but per that Reddit comment, your reducers don't actually "own" the state shape any more. Instead, the action creators do, and there's nothing stopping them from feeding in data that doesn't make sense for a given action type or reducer.
As I talked about in The Tao of Redux, Part 1 - Implementation and Intent, one of the key intents behind the creation of Redux was that you should be able to look at a log of dispatched actions and understand where/when/why/how your state was updated. While Redux itself doesn't care what the action.type field actually contains, an action history log will make more sense to you (or some other developer) if the dispatched actions are meaningfully named. Seeing 10 "SET_STATE" actions in a row tells you nothing useful about what's going on, and while you could look at the contents of each action and the resulting diff, an action type like "EXPEL_STUDENT" means a lot more just by reading it. In addition, unique action types can also be traced to where they're used in specific places in the codebase, thus helping you isolate where the data is coming from.
Hopefully that helps answer some of your questions. If you'd like to discuss things further, I usually hang out in the Reactiflux chat channels on Discord evenings US time. Be happy to have you drop by and chat sometime!
Try using my lib redux-light. You'll need to use store.setState and store.getState
similar to React.Component.setState without reducers at all, and without other boilerplate.
Related
What is the best practice for deriving data in componentDidUpdate, pushing that into the redux store, but also then using that data to further derive more data, and again push that into the redux store?
Given redux is synchronous, I was hoping to be able to access the updated store straight after I done the dispatch, but I am assuming that because it is still within the componentDidUpdate, props would not have been updated with the new store data via mapStateToProps. i.e. I tried -
componentDidUpdate(){
this.determineAndStoreA()
this.doSomethingElse(this.props.A) // hoping this would be the updated A
}
this.props.A is the old value still
should I being something more like -
componentDidUpdate(){
const a = this.determineA()
this.storeA(a);
this.doSomethingElse(a) // using a local copy of A
}
With some further digging, I found a similar question in https://github.com/reduxjs/react-redux/issues/291 . Dan recommends to react to the action in the reducer, but that feels like I now have to combine the logic of determining A, determining B from A, storing A and B at same time?
I think I see what you are struggling with. First let me try to describe more fully the scenario I think you are dealing with.
You have A and B in your store. C is derived from A and B, so if A changes, you want to re-derive C and put it in your store. You could then have component prop E that is derived from C and D. Action x changes A and now you need the new value for C or E and it is difficult to do it in one step without making the reducers overly complicated.
There are three main approaches I have used for scenarios like this and which one I would recommend depends on the specifics of the scenario. All of these are just different approaches for how/when/where to determine C (and could be extended to further derivations like E).
Approach 1
Calculate C in middleware designed for dealing with side effects of actions.
This is the approach most similar to what you already have. It just moves it out of a component so you aren't fighting against the interaction between your store and when lifecycle methods receive the new information. For this approach, I would use Redux Saga and have it handle calculating C in response to action x and dispatching the action to update the store with the new value for C. It's easy to do this (if you're past the initial Redux Saga learning curve) since Redux Saga receives the action after the store has already been updated. The main downside of this approach is that your store will temporarily be in an inconsistent state where C doesn't yet reflect the change to A. For some scenarios this matters and for some it doesn't. The other potential downside is that if you aren't already using Redux Saga, it can be a little intimidating at first because the use of generator functions and yield and the whole mindset when using Redux Saga can feel a bit alien (but I really love using Redux Saga for complex async scenarios).
Approach 2 Calculate C in an action creator.
This generally involves using a thunk for the initial response to action x and the thunk can then get B from the store, calculate C and dispatch an action with a payload that contains two pieces -- one used by the reducer for A and one used by the reducer for C. The main downside with this approach is if there are multiple ways that the dependencies for C (i.e. A and B) can change, this can become a complicated way to organize things, but for some scenarios it can work fine. I have mostly moved away from this approach in favor of the other two approaches.
Approach 3 Don't put C in your store.
If C is fully derived from other things in your store, then it doesn't really need to be in the store. C is just a view on other information in your store. You can use a selector for this purpose instead. If it is expensive to derive C, then use a memoized selector. In the more complicated case of also having E that is derived from C and D, a selector for E can leverage the selector for C while getting D from the store. This is the approach I generally favor unless there is a compelling reason for C to be in the store. This approach allows your reducers to stay simple and independent while keeping your store in a consistent state.
Other Approaches
There are other ways to do this, but the 3 approaches above are ones I have actually used myself. One other route is to have a reducer that leverages other reducers. In this approach, the reducers for A and B would be used by a reducer that then computes C and returns state with all three pieces of information. There are packages out there for making it easier to compose reducers in this manner, but I haven't used them because I'm not very interested in this approach. For me, it just feels like the wrong place to put this complexity.
You won't get the updated prop in the same cycle as Redux pushes the value in to the store and triggers another update to the component.
Do you really need to trigger both actions from componentDidUpdate?
What you can do is create a new action that is triggered from the action where you stored your A value.
something like this
store.dispatch(saveValueA()).then(() =>
doSomeAction()
);
Your component will be rerender everytimes A would change, and componentDidUpdate will be called.
You can then do something like :
componentDidUpdate(prevProps){
const hasAChanged = this.props.A !=== prevprops.A;
if (hasAChanged) {
this.determineAndStoreA()
} else {
this.doSomethingElse(this.props.A)
}
}
I'm not sure what you'r trying to do, hope it helps.
Question about Redux data flow.
Let's talk about huge enterprice application. Dozens of modules, complex hierarchy of reducers, hundreds of action-types.
Simple flow:
Control dispatches an action(for example, input - typing). This action go through every reducer, go through hundreds of switch-cases, and new state is merged from all of reducers with minimal changes. I think, that we have huge unnecessary overhead in this scenario.
What options we can use to decrease overhead?
Use isolated high-level sub-apps with their own provider.
This option will decrease overheads. But if we need any common features in sub-apps, like account info/notifications/etc, we should duplicate it.
Use asyncReducers for code-splitting.
This option, also will decrease overheads, but it is not recommended.
Make a reducers with action-filters. In this case, we add additional information to each action, which reducer should process it.
This option also decrease number of swithces and complexity of newState merging.
But in option 3 I can't understand one thing.
We have control, which is connected to concrete part of state.
99% of actions are processed by single reducer.
Each action at reducer is processed by "case function", which is moved to separate logic-module.
We have action-creator which knows concrete reducer which process this action, and has access to global state.
Why action-creator should dispatch global action, which then will filtrated by cascade of reducers, and then will be swithed through dozens of cases, with not optimal merging of new global state?
Why can't we call case-function in action-creator, compute new global state in optimal way and dispatch it as payload with "SET_GLOBAL_STATE" action type?
I understand that it is anti-pattern. But i can't understand what we lose in this case.
I'll be glad, if someone will explain me what is wrong.
Several thoughts here.
First, per the Redux FAQ entry on splitting logic between action creators and reducers, it's up to you where to put the bulk of your logic. If you prefer to do the actual calculations for what a piece of the new state should be in an action creator, and just have a given slice reducer do return {...state, ...action.payload}, you can.
Second, per the Redux FAQ entry on the performance of calling reducer functions, the cost of calling many reducer functions will generally be very minimal, since most of them will just check the action type and return their existing slice of state. So, the reducer logic will rarely be a performance bottleneck.
Third: yes, it's entirely possible to calculate the entire new app state in an action creator, and have a single generic "SET_GLOBAL_STATE" action. This will allow use of Redux's middleware and time travel debugging, etc. However, this also throws away many of the possible benefits and use cases for Redux. I discussed a lot of the intended purpose for Redux in my blog posts The Tao of Redux, Part 1 - Implementation and Intent and The Tao of Redux, Part 2 - Practice and Philosophy.
To summarize the reasons why we discourage the idea your describing: Redux is intended to make it easy to trace when, why, and how a certain piece of state was updated. While it's up to you to decide how granular you want your actions to be, ideally they should be semantically meaningful. That way, when you read the action history log, you can actually understand the sequence of actions that were dispatched. Also, if you search for a given action type, you should be able to quickly narrow down exactly where in the codebase that action is used. If you only have a single "SET_GLOBAL_STATE" action, it will be almost impossible to determine what part of the code caused a given state update.
I have a saga which runs once every 10 seconds on a POLL action to updated the current state on my GUI.
when a POLL happens I need to make a few calls to walk down the rest interface to get to find the components I care about. There will be a total of 1-5 components, for each of these I need to make a separate rest call for Foo and Bar elements of the components.
Then at some point I need to do some summations, combining the Foo and Bar data together to have the structure expected by my table for listing components, calculating some totals across all components in my dashboard etc. None of the work is cpu intensive, but it adds up to a decent bit of code since I have so many things that need tweaked.
Currently I'm doing all of this in the Saga, but I'm not sure if this is considered bad practice? I feel like reducers are the general 'go to' place for data tweaking, but it feels odd throwing an action with such a large payload, all the responses from every call in a saga, since much of the rest response is data I don't care about. I also like doing all the processing in the saga so I can decide at the end of everything rather to pass an error action to show an error to the user or pass a success action which clears any previous errors, some of the decision for rather I want to clear the action requires more processing of the data.
My only concern is that the generator is getting rather large, with lots of helper methods that feel a little out of place in a saga class to do the processing (their need to be moved to a utils class no matter what I think). The processing isn't too expensive and I am using generators so I don't think the processing will have a noticeable affect on saga's 'threading'. Still, If there is a recommended best practice I want to stick to that. Am I breaking from standard practices doing all of my tweaking of the data in my saga and sending to the reducer a per-formatted object for it to store into the state without any other processing?
This is really a specific case of a common question that is addressed by the Redux FAQ on "where should my business logic live?". Quoting that answer:
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.
There's nothing wrong with having logic on the "action creation" side (whether it be in components, thunks, sagas, or middleware) that does a lot of work to prepare and format data, and having the reducer simply store what was included in the action. On the flip side, having more logic on the reducer side can mean that time-travel debugging will re-run more of your actual code, giving you more chances to edit and retry behavior.
Overall, it sounds like what you're doing is perfectly reasonable.
I've been using Flux first and Redux later for a very long time, and I do like them, and I see their benefits, but one question keeps popping in my mind is:
Why do we decouple actions and reducers and add extra indirections between the call that will express the intent of changing the state (action) and the actual way of changing the state (reducer), in such a way that is more difficult to provide static or runtime guaranties and error checking? Why not just use methods or functions that modify a state?
Methods or function will provide static guaranties (using Typescript or Flow) and runtime guaranties (method/function not found, etc), while an action not handled will raise no errors at all (either static or runtime), you'll just have to see that the expected behavior is not happening.
Let me exemplify it a little better with our Theoretical State Container (TSC):
It's super simple
Think of it as React Component's state interface (setState, this.state), without the rendering part.
So, the only thing you need is to trigger a re-render of your components when the state in our TSC changes and the possibility to change that state, which in our case will be plain methods that modify that state: fetchData , setError, setLoading, etc.
What I see is that the actions and the reducers are a decoupling of the dynamic or static dispatch of code, so instead of calling myStateContainer.doSomethingAndUpdateState(...) you call actions.doSomethingAndUpdateState(...), and you let the whole flux/redux machinery connect that action to the actual modification of the state. This whole thing also brings the necessity of thunks, sagas and other middleware to handle more complex actions, instead of using just regular javascript control flows.
The main problem is that this decoupling requires you to write a lot of stuff just to achieve that decoupling:
- the interface of the action creator functions (arguments)
- action types
- action payloads
- the shape of your state
- how you update your state
Compare this to our theoretical state container (TSC):
- the interface of your methods
- the shape of your state
- how you update your state
So what am I missing here? What are the benefits of this decoupling?
This is very similar to this other question: Redux actions/reducers vs. directly setting state
And let me explain why the most voted answer to that question does not answer either my or the original question:
- Actions/Reducers let you ask the questions Who and How? this can be done with the our TSC, it's just an implementation detail and has nothing to do with actions/reducers themselves.
- Actions/Reducers let you go back in time with your state: again this is a matter of implementation details of the state container and can be achieve with our TSC.
- Etc: state change orders, middleware, and anything that is currently achieved with actions/reducers can be achieved with our TSC, it's just a matter of the implementation of it.
Thanks a lot!
Fran
One of the main reasons is that constraining state changes to be done via actions allows you to treat all state changes as depending only on the action and previous state, which simplifies thinking about what is going on in each action. The architecture "traps" any kind of interaction with the "real world" into the action creator functions. Therefore, state changes can be treated as transactions.
In your Theoretical State Container, state changes can happen unpredictably at any time and activate all kinds of side effects, which would make them much harder to reason about, and bugs much harder to find. The Flux architecture forces state changes to be treated as a stream of discrete transactions.
Another reason is to constrain the data flow in the code to happen in only one direction. If we allow arbitrary unconstrained state modifications, we might get state changes causing more state changes causing more state changes... This is why it is an anti-pattern to dispatch actions in a reducer. We want to know where each action is coming from instead of creating cascades of actions.
Flux was created to solve a problem at Facebook: When some interface code was triggered, that could lead to a cascade of nearly unpredictable side-effects each causing each other. The Flux architecture makes this impossible by making every state transition a transaction and data flow one-directional.
But if the boilerplate needed in order to do this bothers you, you might be happy to know that your "Theoretical State Container" more or less exists, although it's a bit more complicated than your example. It's called MobX.
By the way, I think you're being a bit too optimistic with the whole "it's an implementation detail" thing. I think if you tried to actually implement time-travel debugging for your Theoretical State Container, what you would end up with would actually be pretty similar to Redux.
I could a have a flux action like this:
{type: 'KILL', payload: {target: 'ogre'}}
But I am not seeing what the difference is between having a method on a class People (wrapping the store) like this,
People.kill('ogre')
IF People is the only receiver of the action?
I see that the flux dispatcher gives me two advantages (possibly)
The "kill" method can be broadcast to multiple unknown receivers (good!)
The dispatcher gives me a handy place to log all action traffic (also good!)
These might be good things sure, but is there any other reasons that I am missing?
What I don't see is how putting the actions in the form of JSON objects, suddenly enforces or helps with "1-way" communication flow, which is what I read everywhere is the big advantage of having actions, and of flux.
Looks to me like I am still effectively sending a message back to the store, no matter how I perfume the pig. Sure the action is now going through a couple of layers of indirection (action creator, dispatcher) before it gets to the store, but unless I am missing something the component that sends that action for all practical purposes is updating whatever stores are listening for the kill message.
What I am missing here?
Again I know on Stack Overflow we can't ask too general a question, so I want to keep this very specific. The two snippets of code while having different syntax, appear to be semantically (except for the possibility of broadcasting to multiple stores) exactly the same.
And again if the only reason is that it enables broadcasting and enables a single point of flow for debug purposes, I am fine with that, but would like to know if there is some other thing about flux/the dispatcher I am missing?
The major features of the flux-style architecture are roughly the following:
the store is the single source of truth for application state
only actions can trigger mutation of the store's state
store state should not be mutated directly, i.e. via assigning object values, but by creating new objects via cloning/destructuring instead
Like a diet, using this type of architecture really doesn't work if you slip and go back to the old ways intermittently.
Returning to your example. The benefit for using the action here is not broadcasting or logging aspects, but simply the fact that the People class should only be able to either consume data from a store and express its wishes to mutate the state of said store with actions. Imagine for example that Elves want to sing to the the ogre and thus are interested in knowing the said ogre is still alive. At the same time the People want to be polite and do not wish to kill the ogre while it is being serenaded. The benefits of the flux-style architecture are clear:
class People {
kill(creature) {
if (creatureStore.getSerenadedCreature() !== creature)
store.dispatch({ type: 'KILL', payload: { target: creature } })
return `The ${creature} is being serenaded by those damn elves, let's wait until they've finished.`
}
}
class Elves {
singTo(creature) {
if (!creatureStore.getCreatures().includes(creature))
return store.dispatch({ type: 'SING_TO', payload: { target: creature } })
return `Oh no, the ${creature} has been killed... I guess there will be no serenading tonight..`
}
}
If the class People were to wrap the store, you'd need the Elves class to wrap the same store as well, creating two places where the same state would be mutated in one way or the other. Now imagine if there were 10 other classes that need access to that store and want to change it: adding those new features is becoming a pain because all those classes are now at the mercy of the other classes mutating the state from underneath them, forcing you to handle tons of edge cases not possibly even related to the business logic of those classes.
With the flux style architecture, all those classes will only consume data from the creatureStore and dispatch actions based on that state. The store handles reconciling the different actions with the state so that all of its subscribers have the right data at the right times.
The benefits of this pattern may not be evident when you only have a couple of stores that are consumed by one or two entities each. When you have tens (or hundreds) of stores with tens (or hundreds) of components consuming data from several stores each, this architecture saves you time and money by making it easier to develop new features without breaking existing ones.
Hope this wall-o-text helped to clarify!
What I don't see is how putting the actions in the form of JSON objects, suddenly enforces or helps with "1-way" communication flow, which is what I read everywhere is the big advantage of having actions, and of flux.
Looks to me like I am still effectively sending a message back to the store, no matter how I perfume the pig. Sure the action is now going through a couple of layers of indirection (action creator, dispatcher) before it gets to the store, but unless I am missing something the component that sends that action for all practical purposes is updating whatever stores are listening for the kill message.
What I am missing here?
Facebook Flux took the idea from the event driven GUI systems.
In there even if you move your mouse you get messages. This was called message loop then, and now we have actions dispatching.
Also, we have lists of subscribers inside stores.
And it is really the same principle in Redux where you have one store, while in Flux you may have multiple stores.
Now little mathematics. Having 2 components A and B you need to have just a few possible update chains A updates B and B update A, or self-update (non including in here the updates from outside of the app). This is the possible case.
With just three components we have much more possible chains.
And with even more components it gets complicated. So to suppress the exponential complexity of possible components interaction we have this Flux pattern which in nothing more than IDispatch, IObservable if you worked with these interfaces from some other programming languages. One would be for spitting the actions, and the other for entering the listener's chain that exists inside the store.
With this pattern, your React code will be organized in a different way than common React approach. You will not have to use React.Component state anymore. Instead, you will use the Store(s) that will hold the application state.
Your component can only show the desire to mutate the application state by dispatching the action. For instance: onClick may dispatch the action to increment the counter. The actions are objects with the property type: that is usually a string, and usually in upper case, but the action object may have many other props such as ID, value,...
Since the components are responsible for rendering based on the application state we need somehow to deliver them the application state. It may be via the props = store.getState() or we may use the context. But also check this.
Finally, it is even not forbidden that component uses the internal state (this.state) in case this has no impact on the application. You should recognize these cases.