This is one way of writing actions when using thunk, which results in reducers that are very simple.
getCurrentUserPicture(){
return (dispatch,getState) => {
dispatch({type: "isloading", isLoading: true}); // shows a loading dialog
dispatch({type: "errorMessage"}); // clear errorMessage
dispatch({type: "warningMessage"}); // clear warningMessage
const userId = getState().user.get("currentUser").id;
getUserPicture(userId) // get from backend
.then(picture => {
dispatch({type: "userPicture", picture});
dispatch({type: "isLoading", isLoading: false});
}
)
.catch(e=>{
dispatch({type: "errorMessage", e});
dispatch({type: "isLoading", isLoading: true});
}
)
}
}
With the reducer including the likes of:
export reducer(state = initialState, action = {}) {
switch(action.type) {
case "isLoading":
return state.set("isLoading", action.isLoading)
Here is another approach where the actions are "cleaner" but the reducers are more complex:
getCurrentUserPicture(){
return (dispatch,getState) => {
dispatch({type: "gettingCurrentUserPicture", true});
const userId = getState().user.get("currentUser").id;
getUserPicture(userId)
.then(picture => {
dispatch({type: "retrievedCurrentUserPicture", picture});
}
)
.catch(e=>{
dispatch({type: "errorRetrievedCurrentUserPicture", e});
}
)
}
}
In the reducer for the above action you would have for example:
export reducer(state = initialState, action = {}) {
switch(action.type) {
case "gettingCurrentUserPicture":
return state.set("isLoading", true)
.delete("errorMessage")
.delete("warningMessage")
Is one approach better than the other?
Actions and Reducers should both be as simple as possible.
This is easier said than done, but by introducing other concepts and patterns such as selectors, sagas and even simple utility functions/classes, the complexity can be greatly reduced on both actions and reducers.
I know the term "broadcast" is really frowned upon in the flux world - but I find it helps me refine what should belong in my actions. The action should "broadcast" to the world what just happened - it doesn't personally care what is to follow, or how many reducers choose to respond to it - its just the messenger. ie: it does not care what the app looks like after said action.
My opinion is that business logic/rules either belong here directly, or can be channeled here via helper utility functions. See the sections below about async and utility.
It should describe what happened. Describe, describe, describe
Reducers should form a full (ish) representation of your app with the minimal amount of data possible. Where possible, keep your state tree normalized to make sure you are holding minimal state.
My opinion is that these should be a light as possible, and at most should be just basic object manipulation - adding/removing keys or updating values.
What should the state look like based on the description I just received? (from the action)
It sounds crazy, but rubber duck debugging (in this case rubber duck programming) really helps for planning your redux structure.
I will literally speak (sometimes even outloud) through the steps like, for example:
- You hover over the post title and click edit
- The app swaps to the "edit post" page
- You can edit the title field (and the field will update)
- You can edit the body field (and the field will update)
- The app will save every 90 seconds as a draft, a little save icon will appear in the upper right during the auto saves, but you can continue to work
- You can also save by clicking the save button at anytime. During this time you won't be able to edit, and you will be redirected to the post index page
This can loosely be translated into actions and reducers, by looking at what is describing and what is, as a result, a change in state:
The point I am trying to make, albeit long windily, is that its not what should be simpler, its where things (kind of) belong, but most likely you will see that your actions end up being more complex with your reducers less so.
All of the above is easy enough to say, but how do you keep your those business-y actions clean/slim/light/simple/etc ?
At the end of the day, most business logic doesn't have a lot to do with React/Redux - so they can usually be shovelled out into their utility functions or classes. Which is great for a few reasons a) easier to test because it hasn't zero linkage to React/Redux and b) keeps your actions light, and c) more encapsulated - having minimal knowledge of react/redux is not a bad thing.
It's all just Javascript at the end of the day - there is nothing wrong with importing custom business classes or utility functions.
Async typically starts to clutter up actions really quickly. Saga's are really worth a look if you want to clean up your actions, but do introduce a certain learning curve if you're team isn't across generators and whatnot yet. In which case, thunks are still useful, and remember you can bundle up multiple async functions into a single promise that a thunk can play off (which again, that grouped promise can be separated out into a utility function class - like generateSaveSequencePromise()
which might wrap up 4 or 5 async fetches, and return a single promise).
Side note - you should minimize dispatching multiple times from a single stream like in your first example. If possible, try to create a parent action that groups the entire action into one. So using your very first example, it might be:
// action
dispatch({type: "FETCHING_USER_IMAGE", id: userId });
And then your various reducers should clear their message queues, or whatnot if they "hear" that type come through.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With