Skip to content

Instantly share code, notes, and snippets.

@markerikson
Last active July 29, 2021 18:13
Show Gist options
  • Save markerikson/1d2e925c67f4587c4fdffcc413b70816 to your computer and use it in GitHub Desktop.
Save markerikson/1d2e925c67f4587c4fdffcc413b70816 to your computer and use it in GitHub Desktop.
Reactiflux chat: accessing state in thunks

[9:48 PM] jake: Does anyone have a minute to help me think through setting up my actions? I think I'm falling into a rabbit hole of anti patterns.

I'm working on a React Native app. When the user logs in, I want to fetch some initial data from 3 different end points and this data ends up in 3 different (corresponding) reducers. However, the data is related. For example, two of the endpoints are essentially Events and Tags. Tags are accessed through events. So I need to get all events, then once that finishes, I need to iterate through those events and get all tags for each event. I'm not quite sure how to achieve this without accessing the state in my fetchInitialData action.
[9:49 PM] acemarke: so access the state? :)
[9:50 PM] jake: πŸ˜ƒ I stumbled upon this: http://stackoverflow.com/questions/35667249/accessing-redux-state-in-an-action-creator
which emphasizes "Accessing state in action creators is an antipattern and should be avoided when possible" so I was wondering if I'm approaching the data fetching incorrectly (because I assume this type of initial fetching is somewhat common)
Accessing Redux state in an action creator?
Say I have the following: export const SOME_ACTION = 'SOME_ACTION'; export function someAction() { return { type: SOME_ACTION, } } And in that action creator, I want to access the global ...
[9:50 PM] acemarke: I knew, I knew you were gonna link to that quote
[9:50 PM] jake: πŸ˜„
[9:50 PM] acemarke: I'm half-tempted to edit it out of exstence
[9:51 PM] acemarke: I obviously respect Dan in all kinds of ways, but either I don't understand what he's really trying to say there, or I think he's wrong
[9:52 PM] jake: Alright. Well I'll keep going down the path of adding state into the action creator and see how it goes. Thanks so much for the help and quick responses!!
[9:53 PM] acemarke: sure
[10:29 PM] Francois Ward: I don't think he's wrong. Your UI generates actions, an event log, that gets reduced to a state that is used to generate/update a new UI. The only reason to use the state in an action creator is to get the interim computations that a previous action did within an action, which essentially means you're using the reduced state to create more actions...which doesn't make a whole lot of sense and breaks the 1 way nature of the architecture.

It also means you're doing a lot more in the reducer than just reducing the action log to a state, breaking its semantic.

Finally, even if you needed it, when the state updates, the UI will update and you can get the new version in componentWillReceiveProps, from which you can dispatch the new actions with it, keeping the data flow.
[10:30 PM] Francois Ward: its a nice escape hatch when you REALLY need it, but its excessively rare.
[10:34 PM] Francois Ward: the flow is meant to be UI -> actions -> reducers -> UI, not UI -> actions -> reducers -> actions -> reducers -> UI
[10:36 PM] acemarke: @Francois Ward : lemme paste in a typical example of one of my thunks for reference
[10:36 PM] acemarke: this code is related to editing a list of polyline path points
[10:36 PM] acemarke: items in the list can be moved up and down
[10:37 PM] acemarke: in this particular example, the thunk is doing a quick double-check to make sure it's legal to actually move this waypoint down in the list:
[10:37 PM] acemarke:

export function moveWaypointDown(waypointID) {  
    return (dispatch, getState) => {  
        const state = getState();  
  
        const isItemAffectedByEditing = selectIsItemAffectedByEditing(state, {itemID : waypointID}, "Node");  
  
        // Sanity check - only dispatch if the parent is being edited  
        if(isItemAffectedByEditing) {  
            const session = getEditingEntitiesSession(state);  
            const waypoint = getModelByType(session, "Node", waypointID);  
            const routeLeg = waypoint.parent;  
            const route = routeLeg.parent;  
            const numLegs = route.orderedLegs.count();  
  
            // Sanity check - only move down if not the last item  
            // NOTE: 1-based numbering (for now)  
            if(routeLeg.legSequenceID < numLegs) {  
                dispatch({type : MOVE_WAYPOINT_DOWN, payload : {waypointID}});  
            }  
        }  
    }  
}  

[10:38 PM] acemarke: I suppose this may not be the best example, because in this case I think I actually have some UI logic that would disable the button in that case anyway
[10:38 PM] acemarke: but it's an example
[10:38 PM] Francois Ward: why even allow it to get that far if its not legal? You'd be able to know before even triggering the event.
[10:39 PM] Francois Ward: also, why not just pass the info needed from the component? If it needs to know info about its context the moment it dispatch an action...then just give it that info.
[10:39 PM] Francois Ward: you'll also be able to make the UI better as a result.
[10:39 PM] acemarke: lemme try another one that maybe illustrates the point better
[10:39 PM] acemarke:

export function insertWaypointBetween(firstWaypointID, secondWaypointID) {  
    return (dispatch, getState) => {  
        const state = getState();  
        const session = getEditingEntitiesSession(state);  
        const {Node, RouteLeg} = session;  
  
        const firstWaypoint = Node.withId(firstWaypointID);  
        const secondWaypoint = Node.withId(secondWaypointID);  
  
        const firstLeg = firstWaypoint.parent;  
        const secondLeg = secondWaypoint.parent;  
  
        const route = firstLeg.parent;  
  
        // The math part - calculate a lat/lon halfway between these two waypoints  
        const firstLatLon = new LatLon(firstWaypoint.latitude, firstWaypoint.longitude);  
        const secondLatLon = new LatLon(secondWaypoint.latitude, secondWaypoint.longitude);  
        const midpointLatLon = firstLatLon.midpointTo(secondLatLon);  
  
        const {lat, lon} = midpointLatLon;  
        // Temporarily define a fractional sequence ID that will put the  
        // new leg in between the other two  
        const newLegSequenceID = secondLeg.legSequenceID - 0.5;  
  
        dispatch(insertWaypoint(lat, lon, firstWaypoint.altitude, firstWaypoint.altType, newLegSequenceID));  
    }  
}  

[10:40 PM] acemarke: so it does some calculations based on some data already in the state, and dispatches an action containing those calculations
[10:42 PM] Francois Ward: So, given a fully functioning UI with a given state (overloaded term here: I mean the props and context), you would be unable to generate a valid event log with this architecture.
[10:42 PM] Francois Ward: your UI is now tightly coupled to your reducer.
[10:42 PM] acemarke: ???
[10:43 PM] Francois Ward: it can't behave without a computed store.
[10:43 PM] acemarke: and?
[10:43 PM] Francois Ward: looking at the UI and the event log, you also can't tell where data came from.
[10:44 PM] Francois Ward: and, the whole benefit of Redux over other architectures is being able to pinpoint a bug and where they come from just by their nature, as well as being able to change one piece without affecting the others.
[10:44 PM] acemarke: I'll be honest - I really have no understanding of how you approach your logic
[10:44 PM] Francois Ward: here, you'd honestly be in a better shape using MobX
[10:44 PM] acemarke: part of the time I think you're saying put everything in reducers, part of the time you're not
[10:45 PM] acemarke: how would you structure this kind of feature?
[10:45 PM] Francois Ward: Given a UI, the UI generates an action log. The widget in question here, what info does it need when the event happens to generate a valid action that the reducer can use?
[10:45 PM] Francois Ward: Well, the answer to that question is what the component should get as props.
10:46 PM] acemarke: UI-wise, I have a connected list component, rendering a list of connected children, each receiving the ID of their item
[10:46 PM] Francois Ward: then pass them down to the action creators
[10:46 PM] acemarke: each child is pulling the data for their item out of state
[10:47 PM] acemarke: standard list rendering of normalized data
[10:47 PM] acemarke: the list children, in this case, each have a mini-form containing fields like lat/lon/alt, and buttons to move the entry up/down, insert before/after, delete
[10:48 PM] Francois Ward: yeah, thats pretty standard.
[10:49 PM] acemarke: I'm slightly annoyed at the critique of code that's absolutely working great for me right now, but I'm also genuinely curious what your approach would be, since you seem to think that that code is completely wrong
[10:49 PM] Francois Ward: I'm just saying I don't think Dan Abramov is wrong πŸ˜ƒ
[10:50 PM] Francois Ward: In an Elm app (which is very close to Redux, except that you only "connect on top", so to speak), you don't even have a getState function to use as an escape hatch at all. This isn't too different.
[10:50 PM] Francois Ward: UI generate action, update function reduces it to a single thingy, thingy recreates UI from the top. Any data needed by the UI to generate action comes from there.
[10:51 PM] Francois Ward: In JS, since it's not really as pure, we have escape hatches for when the model doesn't work and to save on some verbosity, but 99% of the time it works.
[10:51 PM] Francois Ward: anti-patterns don't mean they're always bad, just that you should think twice before doing it...and in the cases you showed, you didn't "have" to do it that way.
[10:51 PM] Francois Ward: its totally fine if you disagree, the world can deal with multiple opinions.
10:52 PM] acemarke: I absolutely respect Dan, and I definitely respect you, I'm just A) not seeing the problem with this code, and B) not understanding how you or someone would would tackle it differently
[10:53 PM] Francois Ward: I said it already: whatever data you need to generate the computed payload of your action (which is a side effect of the UI when you think about it), needs to come from the UI.
[10:53 PM] Francois Ward: top -> down.
[10:53 PM] acemarke: well, in this case, the component that kicked off the action definitely doesn't have all the pieces
[10:53 PM] Francois Ward: you have a hook to get state that isn't an anti-pattern. mapStateToProps
[10:53 PM] acemarke: it only knows about the attributes of its own item
[10:53 PM] Francois Ward: Then give it the missing piece.
[10:54 PM] Francois Ward: it doesn't even need to get it as a prop. It can get it from mapStateToProp or context.
[10:54 PM] Francois Ward: (the later is obviously a bit more sketchy)
[10:55 PM] acemarke: that implies a lot more re-rendering
[10:55 PM] acemarke: perf isn't a big concern for me atm, but that's a whole lot of additional overhead for not a lot of gain
10:55 PM] Francois Ward: you're already passing an id and getting state from it to avoid unecessary rendering
[10:55 PM] Francois Ward: you can get the rest of the data the same way, with the same result
[10:57 PM] acemarke: I dunno. Maybe it is that I haven't touched Elm or observables. Maybe it's something about thinking in terms of the application vs thinking in terms of isolated components.
[10:57 PM] Francois Ward: the gain is actually pretty huge: knowing that changing a reducer will never affect your app's logic.
[10:57 PM] Francois Ward: thats pretty powerful when refactoring.
[10:57 PM] acemarke: I also don't get the connection you're making there
[10:57 PM] acemarke: ultimately, what's getting dispatched looks like {type : INSERT_WAYPOINT, payload : {attributes : {lat, lon, alt, id, order}}}
[11:01 PM] JimBolla: I don't like the idea that I have give my UI extra data just to pass along to action creators. Components should only be responsible for rendering UI. Application behavior should be in the app layer. IMO action creators should be as self contained as possible. I want as much of my apps behavior out of React as possible. The UI is always the layer most likely to get wholesale replaced.
[11:01 PM] acemarke: Like I said, I think a bit of this is thinking in terms of the app vs in terms of components
[11:02 PM] acemarke: I'm rebuilding an application I built once previously. I know what the pieces are. I have a few components that are very generic and reusable. I have some other components that are a bit more specific, need to maintain internal state, but can be reused as well.
[11:03 PM] acemarke: and then I've got a bunch of pieces that are totally specific to parts of the app, like these polyline point editors
[11:03 PM] acemarke: and yeah - while React isn't quite a dumb templating layer, I'd rather keep the component focused on displaying the data and translating some input to a prop callback
[11:04 PM] Francois Ward: they're not just displaying data though. They're generating events with context.
[11:04 PM] Francois Ward: (not talking about react context here)
[11:04 PM] Francois Ward: I mean, don't get me wrong: there's an entire school of thought around the store being a totally free for all, use anywhere thingy.
[11:05 PM] acemarke: right. and given the other general constraints in my app, I know that when that action creator is called, "editing mode" is on, there's a bunch of position data in the "editing" slice, and I can go grab some of it and do calculations
[11:05 PM] Francois Ward: thats exactly what brought MobX in
[11:06 PM] Francois Ward: so my question would be: if you don't care about the unidirectional data flow and are willing to shortcircuit it anywhere, and making action creators depend on the store...why not go the whole way and save yourself the trouble?
[11:06 PM] acemarke: I feel like you're making assumptions about my mindset that aren't true
[11:06 PM] JimBolla: Because I'm practical πŸ˜‰
[11:06 PM] Francois Ward: im not making any assumptions πŸ˜ƒ Else I wouldnt be asking.
[11:06 PM] acemarke: why do you say "I don't care about uni-dataflow"?
[11:07 PM] Francois Ward: because you're shortcircuiting it
[11:07 PM] acemarke: because I'm doing some reads from the store in the process of dispatching?
11:07 PM] Francois Ward: your app has a store, a UI, and actions, and you're accessing your data at all points.
[11:08 PM] Francois Ward: reducers obviously have access to it, your UI has mapStateToProps, and your actions have access to it too.(edited)
[11:08 PM] Francois Ward: aside for the dumb components, there's no part of your app that isn't playing with it.
[11:09 PM] Francois Ward: so, and this is a 100%, honest question...what are you gaining over MobX here?
[11:09 PM] acemarke: devtools, middleware, sagas, ...
[11:10 PM] Francois Ward: all of which aren't dependent on the architecture (well they are, but the architectures being compared in my question both allow for it)...so basically its just for ecosystem maturity?(edited)
[11:11 PM] Francois Ward: note that saying yes is a totally legitimate answer
[11:11 PM] Francois Ward: im not judging πŸ˜ƒ
[11:11 PM] acemarke: and I still don't get what and why you're accusing me of doing here
[11:11 PM] Francois Ward: im just trying to understand
[11:11 PM] Francois Ward: not accusing of anything, and sorry if I said anything that sounded that way
[11:12 PM] Francois Ward: look at my first name. English no first language.
[11:12 PM] Francois Ward: (you spellchecked by blog posts before...you know that, hahaha)
[11:12 PM] acemarke: it's not the language that's the issue, it's the statements and ideas you're tossing out :)
[11:12 PM] JimBolla: Consider this... an action creator that needs 4 pieces of data, some of that data lives in the store. There are 5 components that might call that action creator. Given your suggestion, each component then has to select all the data needed to pass to that action creator IF it calls it. What if selecting that data is expensive and that chance that action creator is called is low? Now you're needlessly slowing your app. What if a new requirement dictates that action creator now needs an addition piece of data that lives in the store. Now you have to go update all the components to ensure they pass the additional parameter.
[11:13 PM] JimBolla: IMO those are both worse sins than the fact that both components and action creators are aware of the store.
[11:15 PM] Francois Ward: Given a real world scenario of that example, most of the time this will be a situation where the data would belong in context, or the data doesn't change often and isn't expensive to select.

If it is one of the very rare scenario where there's really no better way, then yes for sure, thats why the escape hatch is for. To me, an anti-pattern just means "something you should have one hell of a good reason to do"
[11:15 PM] Francois Ward: I would never advocate to remove it altogether.
[11:17 PM] Francois Ward: even React itself is basically "functional components with escape hatches when you need to deal with real world realities"
[11:18 PM] Francois Ward: but there's a pretty thick line between "use when you have to" and "knock yourself out, its free!"(edited)
[11:21 PM] acemarke: so going back to the original example: you're basically saying that the action creator's signature should look something like insertWaypointBetween(allDataForFirstPoint, allDataForSecondPoint), instead of insertWaypointBetween(firstWaypointID, secondWaypointID) ?
[11:22 PM] acemarke: actually, come to think of it. Looking back at that code, that actually winds up getting called from another thunk
[11:22 PM] acemarke: like, say, addWaypointBefore(waypointID) {}
[11:22 PM] acemarke: which is what is actually called from the component
[11:23 PM] acemarke: I dunno. I feel like this is mostly arguing semantics. (And I've hated that word for most of the last year, because reasons.)
[11:24 PM] acemarke: I get the basics of FP, but I've never gone deep into it
[11:24 PM] acemarke: I've always been a pragmatist, not a purist. Figure out a way to make it work first, then figure out a nicer way to handle it.
[11:24 PM] acemarke: maybe I'm doing all this entirely wrong, but it's certainly working at the moment.
[11:25 PM] acemarke: (and it's definitely way better than the first iteration I wrote several years ago)
11:25 PM] JimBolla: agreed
[11:25 PM] Francois Ward: everything can be made to work, its just a matter of what tradeoffs you're making.
[11:25 PM] acemarke: maybe my lack of unit testing experience is another factor
[11:26 PM] Francois Ward: I'd be lying if I said the unit testing experience after coming from Angular 1 wasn't a big factor in why I ended up picking Redux back then.
[11:26 PM] Francois Ward: when you have a function that given some arguments, always return the same thing...it makes things pretty damn easy.
[11:27 PM] Francois Ward: thunks aren't too terrible even if you use getState though since you can just pass a dummy getState implementation.
[11:27 PM] Francois Ward: just makes it a little harder to reason about when looking at the signature.
[11:27 PM] acemarke: sure, I get the usefulness of pure functions
[11:27 PM] JimBolla: You can always split an action creator into smaller pure functions and test those if you like.
[11:27 PM] acemarke: I suppose I could break out some of this logic if I tried
[11:27 PM] acemarke: yeah
[11:28 PM] JimBolla: My goal is to get as much code out of the components as possible, this includes shuttling parameters.
[11:29 PM] acemarke: pretty much that
[11:29 PM] JimBolla: So either that goes into the action creator, or some new concept which calls action creators but can also access state so that my action creators can wear their purity ring?
11:31 PM] Francois Ward: as you should
[11:31 PM] Francois Ward: since a reducer is basically "previous state + as little info as possible to get new state"
[11:32 PM] Francois Ward: so we're in total agreement there
[11:32 PM] acemarke: in this case, the thunk is sorta-kinda doing some reducer like things, it's just that stuff like "generate a new UUID" should stay outside
[11:32 PM] JimBolla: I think there's diminishing returns on purity. Much like OOP fans go to far. Same thing happens with FP. Same thing happens in SQL land. Sometimes your best, clearest, fastest, easiest-to-test tool is a imperative method.
[11:33 PM] JimBolla: The magic is in getting a sense of where those boundaries are.
[11:34 PM] acemarke: going back to an earlier question: why am I using Redux instead of MobX?
[11:34 PM] acemarke: predictability and explicitness are a huge part
[11:34 PM] acemarke: Redux has forced me to think much more carefully about what state I have, and where it should live
[11:35 PM] acemarke: as programmers, we all know how rare is to write some meaningful chunk of code and have it work perfectly the first time
[11:35 PM] Francois Ward: yeah, this is where i have issues here...once you have a construct that can do side effects and has access to the entire app's state...all bets are off on predictability IMO.
[11:35 PM] Francois Ward: you have 1 construct that can do anything and everything
[11:36 PM] Francois Ward: or rather, you have "UI" and "everything else"
[11:36 PM] Francois Ward: and i think there's better architectures to achieve that separation.
[11:37 PM] acemarke: working on this app, I've had several occasions where I was able to think through what needed to happen, where data was coming from, how it needed to be transformed, and so on. And once I implemented those ideas, barring minor typos and such, they worked as planned. Maybe it's a bit of maturation as a programmer in general, but I love the fact that Redux made me think through what I was doing, and because I understand the exact dataflow involved, what I wanted to happen happened.
[11:42 PM] acemarke: I get that you can run arbitrary code inside a thunk. Or really a saga, for that matter.
[11:43 PM] acemarke: no, my code isn't anywhere near as pure as it could be
[11:43 PM] acemarke: it's at least a step in the that direction
[11:43 PM] Francois Ward: well, you'll always need to pick a construct to be impure... we don't have algebraic effects, and attempts to fake it (see: redux-loop) are too verbose in JS syntax.
[11:47 PM] acemarke: one other thought: in some ways this goes back to a quote I saw in the issues list a few weeks ago:

Redux is a generic framework that provides a balance of just enough structure and just enough flexibility. As such, it provides a platform for developers to build customized state management for their use-cases, while being able to reuse things like the graphical debugger or middleware.

[11:48 PM] acemarke: I've seen all different kinds of structures and uses in different people's apps
[11:49 PM] acemarke: plain JS, Immutable.js, seamless-immutable, thunks, sagas, observables, promises, middleware, constants, strings, combineReducers, switch statements, lookup tables, Elm-alikes, you name it.
[11:50 PM] acemarke: I've seen seriously complex reducer logic, and I've seen people using Redux as a dumb key/value store, like that one guy the other day who posted an article with one reducer and a single "UPDATE" action that was just return {...state, ...action.payload}
[11:51 PM] acemarke: I certainly don't think that it's a good idea, but most of the infrastructure still works with that
[11:51 PM] Francois Ward: hahaha, I had to fight against that one the other day.
[11:51 PM] Francois Ward: mainly because at that point, just use a mutable store and pass it in and call it a day >.>
[11:51 PM] Francois Ward: That reminds me.
11:52 PM] Francois Ward: gonna change topic because I dont think we're gonna get any further with the other one... have you seen Hyper's (the Electron terminal emulator)'s effect middleware?
[11:52 PM] acemarke: I skimmed their source once because I saw they used Redux, and I was curious how they were composing things
[11:52 PM] acemarke: but don't remember what they're doing
[11:53 PM] Francois Ward: they allow actions (not action creators. Actually action objects) to have an effect method that the middleware execute, so that can "fire and forget" arbitrary things. Then they have a plugin middleware that allows anyone to hook into the action chain, and nuke an effect from orbit or add new ones as needed.
[11:53 PM] Francois Ward: eg: if you wanted to replace an effect with your own.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment