Create a gist now

Instantly share code, notes, and snippets.

Reactiflux #redux discussion: top-down single connect vs multiple lower connects

May 12, 2016

[8:23 PM] jrajav:: Heya. I'm wondering what the function of components really are. Just don't fully understand - why not just pass in the state with something like store.subscribe( () => render( , document.getElementById('root') ) ) ?

[8:24 PM] jrajav:: Passing in dispatch functions as well

[8:24 PM] jrajav:: Then, split apart the state as appropriate further down for each subcomponent

[8:25 PM] jrajav:: If all of the components are pure, stateless functional components this approach should still be just as performant, right?

[9:58 PM] acemarke: @jrajav : idiomatic Redux usage goes for some simple dependency injection. Ideally, your components and your action creators never refer to the store directly

[9:58 PM] acemarke: that makes your code more reusable, and also easier to test

[9:59 PM] acemarke: see http://redux.js.org/docs/FAQ.html#store-setup-multiple-stores

[10:00 PM] acemarke: also, that snippet you wrote looks like the App component would be responsible for passing all necessary state downwards(edited)

[10:00 PM] acemarke: if your state is small enough that you can keep it in one top-level component, you probably don't need Redux to begin with - just use React's component state

[10:01 PM] acemarke: but, with React Redux, any component anywhere inside the can subscribe to the pieces of state that it needs, without a top-level component having to know exactly which descendants need which data

[10:25 PM] jrajav:: Yes @acemarke, but on the other hand you don't know which parts of the component tree depend on which data. Magically grabbing pieces of the state in deeply nested components seems less functional and maintainable to me

[10:26 PM] jrajav:: If you use nested containers to subscribe to pieces of the state, that is

[11:56 PM] acemarke: uh.. there's no "magic" about it

[11:57 PM] acemarke: any container class defined using connect() very clearly specific what pieces of the state it needs

[11:57 PM] acemarke: here's a made-up but typical example

[11:57 PM] acemarke: maybe you have a bunch of users in your state

[11:58 PM] acemarke: you have a top-level presentational component that renders two more presentational components, a LeftSidebar and a RightMainPanel

[11:58 PM] Francois Ward: I think he means that looking at your folder structure, without grepping, stuff can come from anywhere.

[11:58 PM] acemarke: in the LeftSidebar, you might render a connect UsersList

[11:58 PM] acemarke: which would display a list of UserItems

[11:58 PM] acemarke: maybe just user name and a couple other details

[11:58 PM] Francois Ward: which is a pretty common criticism of that model. Cyclejs/Elm do it very much the "everything starts from the top and gets wired down". Its a compromise of pragmatism vs ease of following/purity/testability.(edited)

[11:59 PM] acemarke: then, if you click on a UserListItem to select it, you would want to show a UserDetails component inside the RightMainPanel

[11:59 PM] acemarke: which would also be connected, and grab the details for that user

May 13, 2016

[12:00 AM] acemarke: but the TopLevelComponent rendering the LeftSidebar and RightMainPanel would have no need to know the minutia of that

[12:00 AM] acemarke: if you really want to have your top component be responsible for all data, you can

[12:01 AM] acemarke: but the generally accepted best practice at this point is to connect at multiple points lower in your component tree, closer to where the data is needed

[12:02 AM] acemarke: the original question was asking about the reason for using Provider, as opposed to having a single top-level connected component and nothing but pure stateless components underneath

[12:03 AM] acemarke: oh, and @jrajav : to specifically answer your question, no that would not be more performant

[12:03 AM] acemarke: as the single connected top component would have to re-render itself on every store change

[12:03 AM] acemarke: and every stateless component underneath it would also re-render

[12:04 AM] acemarke: whereas with multiple connected lower-level components, the sub-rendering trees would be much smaller

[12:04 AM] acemarke: and, React Redux does a lot of work to optimize how much a connected component actuall has to re-render

[12:19 AM] jrajav:: Yes @Francois Ward , that's what I meant

[12:20 AM] jrajav:: The "magic" part is connect() itself, grabbing the state in an arbitrary location and exposing it

[12:21 AM] jrajav:: And @acemarke thanks for the info - can I just ask what you mean by re-render exactly?

[12:22 AM] jrajav:: Do you mean that React will actually be re-rendering every single component onto the light DOM?

[12:22 AM] jrajav:: Or just that the render() functions will all be re-run, rather than "short-circuited" by a shouldComponentUpdate?

[12:22 AM] acemarke: as opposed to...?

[12:23 AM] acemarke: if you're worried about having to have a mapStateToProps function dig into state.some.very.nested.field, you can use "selector" functions to encapsulate where in the state tree a particular bit of data is needed

[12:24 AM] jrajav:: No, I am more worried about if I use React + redux + immutable.js and the pattern of passing in the full state object to the single , will updating a single field far down in the tree result in the entire being re-rendered onto the DOM

[12:24 AM] jrajav:: Or if React's virtual dom diffing will take care of that for the most part

[12:25 AM] jrajav:: I don't know how that diffing works, exactly

[12:27 AM] jrajav:: For instance - say that I have and , and I'm passing a subproperty of the full state object pageOne to 1 and pageTwo to 2. Some nested subproperty in pageTwo changes - so changes, and needs to be re-rendered. Nothing in pageOne changes so is called with all the same parameters and results in the same virtual dom tree.

[12:28 AM] jrajav:: I don't think all of the render() calls can avoided because that's not optimized in React yet (right?), for stateless functional components

[12:28 AM] jrajav:: But at the very least, as a result of the virtual dom diffing, none of the light DOM for will be updated.. right?

[12:29 AM] acemarke: the render functions will be re-run, and the corresponding virtual DOM will be re-diffed

[12:29 AM] acemarke: which, if none of the inputs to a given component changed, means wasted effort

[12:30 AM] jrajav:: True, yes - but those are relatively cheap compared to DOM updates

[12:30 AM] jrajav:: And can potentially be mitigated with memoizing wrappers

[12:30 AM] acemarke: ah... hang on a sec, lemme read through what you've got here

[12:30 AM] acemarke: (also, bear with me - my wifi's been acting up lately, and connection is kinda sporadic at times)

[12:30 AM] jrajav:: No problem

[12:31 AM] jrajav:: And to clarify, by light DOM I just mean "actual DOM"

[12:31 AM] jrajav:: (Mostly worked with Polymer and shadow DOM lately so I'm used to calling it light DOM ?? )

[12:32 AM] acemarke: in theory, yeah

[12:32 AM] acemarke: but it can still add up

[12:32 AM] acemarke: which is what connect() does for you, conveniently

[12:32 AM] acemarke: among other things, it does a shallow equality check on the object you return from mapStateToProps, vs what you returned last time

[12:33 AM] acemarke: among other things, it does a shallow equality check on the object you return from mapStateToProps, vs what you returned last time

[12:33 AM] acemarke: so if a given component returns the same contents from MSTP twice in a row, it will skip re-rendering the wrapped-up component

[12:34 AM] acemarke: on the flip side

[12:34 AM] acemarke: for your Page1 / Page 2 example

[12:34 AM] jrajav:: Ah, okay

[12:35 AM] acemarke: assuming no use of connect(), and no self-implemntation of shouldComponentUpdate, a small change to the portion of the state tree that Page2 cares about would cause both Page1 and Page2, and all of their children, to at least go through the render / diff cycle

[12:36 AM] jrajav:: And by self-implementation of shouldComponentUpdate you just mean a memoizing wrapper right?

[12:36 AM] jrajav:: If you memoize a subcomponent does that avoid virtual dom diffing for it, though?

[12:39 AM] acemarke: so, in addition to doing the data->props management for a component, the React Redux connect() function also usually offers a good bit of performance improvement simply by using it

[12:39 AM] acemarke: ah... no, usually SCU is simply doing prop equality checks

12:41 AM] jrajav:: Well a shallow comparison is all a normal memoizing wrapper would do too

[12:41 AM] jrajav:: I don't know enough about how shouldComponentUpdate works to know if returning the same component result instead of render() again would work the same way

[12:41 AM] acemarke: generally SCU is just shallow comparing this.props vs nextProps

[12:41 AM] acemarke: ideally

[12:41 AM] acemarke: no memoization

[12:42 AM] acemarke: SCU doesn't return components

[12:42 AM] acemarke: it's a boolean

[12:42 AM] jrajav:: So there's no way to replicate its behavior with stateless functional components?

[12:42 AM] acemarke: it's literally just "hey component, here's what your incoming props look like. Do you want me to re-render you? Yes/no"

[12:43 AM] acemarke:

shouldComponentUpdate(nextProps) {
  // single-field example.  extrapolate for more props, or do all of them
  return this.props.someField !== nextProps.someField;
}

[12:43 AM] acemarke: no

[12:44 AM] acemarke: if you want React lifecycle methods, you use React.createClass() or MyClass extends React.Component

[12:44 AM] acemarke: functional stateless components, for now, are simply for brevity and clarity

[12:45 AM] acemarke: not only no performance optimizations yet (despite the docs hinting at them coming eventually), but effectively net performance losses because no way to keep one from selectively not re-rendering

[12:46 AM] acemarke: that said, passing any one of those types to connect() gives you connect's benefits either way

[12:47 AM] acemarke: because it's wrapping up your "real" component

[12:47 AM] jrajav:: So theoretically I could just use connect() without ever using it to extract state

[12:48 AM] jrajav:: And it would still give me SCU optimizations over shallow properties (good enough for Immutable, probably)

[12:48 AM] jrajav:: Or, just roll my own higher-order component function that does something similar

[12:49 AM] jrajav:: But there's no other way to optimize renders of stateless components right now(edited)

[12:50 AM] acemarke: does that help clear things up any?

[12:50 AM] jrajav:: Just one thing I'm still not totally clear on. Say I want to eat the cost of re-rendering everything to virtual dom

[12:50 AM] jrajav:: The virtual dom diffing would still hide most of that from real DOM updates right?

[12:51 AM] jrajav:: So that 's "real" DOM counterpart would still never see any of this even with unoptimized stateless components, for instance

[1:00 AM] acemarke: basically, yeah

[1:02 AM] acemarke: yes, absolutely

[1:02 AM] acemarke: there'd just be a fair amount of wasted CPU cycles doing meaningless comparisons

[1:03 AM] acemarke: if you've only got a few components overall, and/or a relatively low volume of actions updating your Redux store, that may not be a concern at all

[1:04 AM] acemarke: if you've only got a few components overall, and/or a relatively low volume of actions updating your Redux store, that may not be a concern at all

[1:04 AM] acemarke: and there's certainly plenty to be said for the old adage of "make it work, make it right, make it fast"

[1:06 AM] acemarke: on the flip side, if you know you're gonna have a bunch of components, or lots of actions (and especially if you're doing something like using controlled inputs that are directly tied to updating the store), then you really will need to cut down on the work being done(edited)

[1:07 AM] acemarke: past my bedtime here. hopefully this information has helped :)

[1:07 AM] jrajav:: So, until React catches up with the stateless components, we really should be using connect(), or some simpler memoizing layer

[1:07 AM] jrajav:: Yes you were a great help @acemarke

[1:07 AM] jrajav:: Thank you so much for your time

[1:08 AM] jrajav:: (And not really memoizing, but React-short-circuiting, or what have you)

9:30 AM] jrajav:: @acemarke I found out that https://github.com/acdlite/recompose 's pure() function will do the same shouldComponentUpdate implementation you mentioned in react-redux's connect()

GitHub

acdlite/recompose

recompose - A React utility belt for function components and higher-order components.

[9:30 AM] jrajav:: Given that, are there any other reasons to prefer connect() for stateless components?

[9:31 AM] jrajav:: If we're initially trying to depend only on passing state through props?

[9:31 AM] jrajav:: Does it provide any other performance optimizations?

9:49 AM] acemarke: Yeah, it does do a couple more. Given the approach you're aiming for, though, I'd probably use recompose. More clarity of intent

[9:52 AM] acemarke: (Admittedly, I still don't get why you seem to be against using connect in multiple places to grab data directly for the components that actually will use that data)

[10:06 AM] jrajav:: I just prefer to write in a more functional manner. The data dependencies are explicit and clear no matter where you are, composition and testing are dead simple.

[10:07 AM] jrajav:: What are the couple more that I should look out for? From the way you say that I guess recompose provides them, too?

[7:58 PM] acemarke: @jrajav : with connect(), there's two major optimizations built-in, and room for one more yourself. It does a shallow-comparison of the return values from your mapStateToProps to see if the latest store change even affects this component (not counting any specific props that might be passed directly from a parent component); it implements an apparently less-known optimization where it actually returns the exact same React.createElement() instance if none of the map/dispatch/merged props have changed; and your own component can still implement shouldComponentUpdate on top of all that.

Dan did a great video walkthrough of most of connect()`'s source code, describing what it does and how it's implemented. The code's evolved a bit since then, but still very informative. It's at https://youtu.be/VJ38wSFbM3A.

[9:11 PM] jrajav:: @acemarke So if we don't use injected state / dispatch and just depend on immutable.js props passed in explicitly for everything, connect() doesn't add anything that isn't in recompose's pure()

[9:11 PM] jrajav:: Right?

[9:11 PM] jrajav:: Because if you have just immutable.js props and you implement a shallow-compare shouldComponentUpdate over those (what pure() does), you're covered

[9:13 PM] acemarke: mostly, yeah

[9:38 PM] acemarke: hmm. hey, @jrajav . the one other thing to consider with all this? I don't think you're going to be getting all that much benefit out of shouldComponentUpdate most of the time, depending on how your component tree is laid out.

[9:38 PM] acemarke: if you're going to be manually passing props down from the top all the time, that means that a majority of the time certainly the top several components in your tree will also be re-rendering

[9:38 PM] acemarke: the ones near the bottom might not be

[9:39 PM] acemarke: but the ones that are delegating a lot of props towards their children will

[9:39 PM] acemarke: the other hassle to think of is having to always track which deeply nested child needs which props, and having to make sure you pass them from A > B > C > D > E

May 14, 2016

[1:24 AM] jrajav:: It makes sense that the path from the root to the leaf needing updating of a component tree will all have to re-render. That's just accurately reflecting what's occurring, isn't it? And how is it possible to avoid that with any kind of organization?

[1:27 AM] jrajav:: And yes @acemarke I can see how tracking those props could be seen as a hassle. Personally I think of it more like this: D contains E and E is simply a part of what it requires to render. Therefore it makes sense that D needs to take in the properties that E needs, too, because really they are also properties that D needs to render. And so on and so forth... I would rather see, as a matter of the component's function signature, exactly the parameters it requires in order to render, and not have components magically pulling their state out of the store at arbitrary points. To me that is akin to using global variables and singleton classes to cheat and pass around state - convenient, yes, but at the cost of your code becoming more difficult to reason about, less testable, less verifiable, and more error-prone.

[1:28 AM] jrajav:: And it's confusing to me that the "best practice" tossed around with react / redux right now seems to be to use that container-component pattern when it's supposed to be a functional paradigm

11:37 AM] acemarke: @jrajav : as I see it, the "container" pattern gives you a few benefits. First, it does allow you to do some splitting of components by conceptual type, if you like to think about them that way - components that are focused on pure layout and organization, vs components that are responsible for fetching data (whether it be from a Redux store, from the server, etc).

[11:41 AM] acemarke: Second, you get performance improvements, because a lot fewer components are forced to re-render compared to a totally top-down approach. Most of your more layout-focused presentational components probably wouldn't be re-rendering at all, and the use of connect() on a component effectively starts a new update tree-ish thing, especially if that connected component isn't being directly passed any props by its parent. So, if the store updates, and the update didn't change any values that component cared about in mapStateToProps, that component will skip re-rendering, and so will its entire subtree. Easy win.

[11:43 AM] acemarke: Third, I would argue that having a connected component pull in the data it needs, right there, makes it a lot easier to trace the actual data flow, as there's fewer layers to go through. If I want to know why I'm getting something wrong in my UserListItem, I'd rather just look at its parent UserList rather than have to trace through TopLevelComponent > MainLayout > LeftSidebar > UserList > UserListITem

[11:44 AM] acemarke: Fourth, the connected component itself becomes a lot more reusable within the context of the application - if I decided I want to move it from the LeftSidebar to the BottomPanel, I can just do so, without having to change the flow of props coming down from the top component

[11:46 AM] acemarke: Fifth, the advice for testing connected components is to export the connected version as the default export, but export the "plain" component as a named export, and only really test the unconnected version. What you care about is how that component responds to its props and lifecycle. How it gets those props shouldn't matter. So, you feed it some props in your test code and verify its behavior there, and maybe test your mapStateToProps function as well if you want, but you can safely assume that connect() will correctly call mapStateToProps when needed, and pass those props to your component when something changes.

[11:50 AM] acemarke: Sixth: yes, in a sense Redux is a "global variable", but on the flip side, idiomatic Redux code never actually references it directly. Everything gets dependency injected. connect() gives you props from the store and a reference to dispatch(), middleware get dispatch() and getState() injected, and since thunks and such are actually middleware, your action creators can get those injected as well. No direct references to the store anywhere in your actual app code other than the two lines where you create your store, and pass it to . That also makes using something like redux-mock-store feasible for testing.

[11:50 AM] acemarke: Finally, I'm kinda confused: what are your concerns with "containers" vs "functional"?

[11:52 AM] acemarke: Ultimately, sounds like we have some differing philosophies on what constitutes good architecture, which is okay. I'm just trying to understand what all the differences in point of view are.

[2:03 PM] Francois Ward: "Fifth, the advice for testing connected components is to export the connected version as the default export, but export the "plain" component as a named export, and only really test the unconnected version. "

[2:04 PM] Francois Ward: Careful about that one, its not that simple... if you have a dumb component that contains a connected component, they now MUST provide a store somehow, OR mock the connected component. Not a problem with Jest, or when you can shallow render.

[2:04 PM] Francois Ward: but it's not always an option.

[2:04 PM] acemarke: yeah, that caveat probably oughta get added to the docs at some point

[2:04 PM] Francois Ward: connected components as children are a pain in the rear. Its a compromise, but its not simply "connect everywhere, its free!"

[2:05 PM] Francois Ward: there's a strong benefit, AND a high cost to it.

May 15, 2016

[11:39 AM] jrajav:: @acemarke Thanks for the very detailed response! And yes, as you mentioned I think a lot of what this comes down to is a difference of opinion on best practices. You think it's easier to reason about and reuse a component that knows how to pull its own properties out of the global state (or a close parent of it does, at least), and I would actually concede that at the component-level but I'd also say that dealing with global state at arbitrary points makes the composition of multiple components and the app itself more difficult to reason about, more difficult to compose (like putting together a puzzle), and more error-prone. The extra work involved in explicitly declaring the data dependencies (in this case, by manually passing parameters down) is good work that avoids all that and makes your application simpler and more maintainable. That's the way that I see it, at least.

[11:41 AM] jrajav:: @acemarke Also, enforcing that React is always just the presentation layer, and has no knowledge of the store at all, and that all components are "dumb", pure, and have no real lifecycle to speak of - they are just functions of their input - that makes them much easier to develop, test, and reuse

[11:42 AM] jrajav:: And can I ask what you meant by this?(edited)

[11:42 AM] jrajav:: > Second, you get performance improvements, because a lot fewer components are forced to re-render compared to a totally top-down approach. Most of your more layout-focused presentational components probably wouldn't be re-rendering at all, and the use of connect() on a component effectively starts a new update tree-ish thing, especially if that connected component isn't being directly passed any props by its parent. So, if the store updates, and the update didn't change any values that component cared about in mapStateToProps, that component will skip re-rendering, and so will its entire subtree. Easy win.

[11:44 AM] jrajav:: From what I understood, if you have a shouldComponentUpdate over a subtree composed of fully dump props and all of the parameters are equal (probably with the aid of Immutable.js to handle deep objects), then that subtree will not be rendered, so I can't understand how this is different with either methodology

[11:48 AM] jrajav:: And to directly answer your question, my concern with "containers" vs. "functional" is that, following from my principles, React should just be the presentation layer, just one large function of the state resulting in the corresponding UI - all the way up to the itself.

[11:50 AM] jrajav:: And thanks for all the counter-opinions, @acemarke! I obviously have my own set of preconceptions already but I'm very new to React and cautious about the nuances I haven't considered. This is very valuable to me.

[11:52 AM] acemarke: @jrajav : sure, good reasonable technical differences of opinion and discussion are usually valuable :)

[11:53 AM] acemarke: so let's see. Per your comments:

[11:54 AM] acemarke: in my case, my opinions are generally influenced by the numerous discussions I've read, as well as the particular needs of my own app

[11:55 AM] acemarke: for example, Dan Abramov's advice at the very beginning of Redux was indeed to only connect your top component, or maybe top couple

[11:57 AM] acemarke: but that advice has distinctly changed as time has gone on, and now the suggested usage is to connect deeper wherever it makes sense. In fact, in his recent optimization pass for the MobX vs Redux benchmark, he found that the "list with 10K items" scenario works best if the parent list gets the IDs of all 10K items, passes the ID to each rendered child item, and the ListItems themselves are also connected and responsible for updating themselves. That way, an update to one list item doesn't modify the set of IDs, therefore the parent list doesn't have to re-render, etc.

[12:00 PM] acemarke: another thought is that while "React is just the view layer" is true, the issue here is that we're trying to figure out how to hook up the data layer and the view layer. A typical connected component will define its mapStateToProps and the plain component in the same file right next to each other, then export default connect(mapStateToProps)(MyComponent). So, it's pretty easy to see what the data needs are for a given connected component.

[12:01 PM] acemarke: I would also argue that even with multiple connected components, the overall UI is still a "function of the state"

[12:01 PM] acemarke: let's see. the performance tree thing...

[12:01 PM] jrajav:: In this case "re-render" just meaning that the list element still needs to iterate over every list item just to find out most of them pass the SCU, right?

[12:02 PM] acemarke: yeah

[12:02 PM] acemarke: parent renders -> children check SCU -> children may or may not re-render

[12:02 PM] acemarke: but the point is that SCU still gets called even if the props are literally identical

[12:03 PM] jrajav:: Right

[12:03 PM] jrajav:: But if the SCU is just an Immutable.js equality check, that boils down to a single === op per call right?

[12:03 PM] jrajav:: Factor in the function calls and yes it's not exactly nothing but not very expensive either

[12:03 PM] acemarke: one per field in props, anyway

[12:04 PM] jrajav:: Right

[12:04 PM] jrajav:: But yes, I can see how this lets you skip over subtrees for rendering

[12:05 PM] acemarke: lemme see if I can clarify my point on the subtrees bit you were asking about

[12:06 PM] acemarke: so as I understand it, in the "one top level component to rule them all" scenario, you're basically doing const mapStateToProps = (state) => state;

[12:06 PM] jrajav:: I'm not entirely sure what you mean by that

[12:06 PM] jrajav:: It's more like there is no mapStatetoProp anywhere

[12:06 PM] acemarke: well, SOMEONE has to use it to grab something from the store...

[12:07 PM] acemarke: or the moral equivalent thereof

[12:07 PM] jrajav:: (edited)

[12:07 PM] acemarke: yeah, that's just connect() without actually calling connect

[12:08 PM] acemarke: I assume you're doing, roughly:

[12:08 PM] jrajav:: The difference being that it's only at this one root element that we ever access the state

[12:08 PM] acemarke: so the same as doing connect(state => state)(App)

[12:09 PM] jrajav:: Well sure, but when it's that simple either way it seems preferable to do it the more obvious, explicit way

[12:09 PM] jrajav:: Otherwise you're using an abstracted function just to do state={store.getState()} once in the whole app

[12:09 PM] acemarke: yeah. anyway, point is, top level component gets the whole state every time, either way

[12:09 PM] jrajav:: Correct

[12:09 PM] acemarke: so. Let's go with my favorite hypothetical app structure

[12:10 PM] jrajav:: Todo? ??

[12:10 PM] acemarke: some kind of LeftSidebar and RightMainPanel(edited)

[12:10 PM] jrajav:: Aww

[12:10 PM] acemarke: NO. NO MORE TODOS.

[12:11 PM] acemarke: and we'll say that the left sidebar shows.... a CurrentUserInfo component, plus a ListOfThings component

[12:11 PM] acemarke: when you select a ThingListItem in the list, the MainPanel shows a details form for that Thing

[12:11 PM] acemarke: which you can edit

[12:12 PM] acemarke: so our state would look like, say: { currentUser: {}, things : { byId : {}, order : [] }, thingBeingEdited : {} }

[12:13 PM] acemarke: cool so far?

[12:13 PM] jrajav:: Yep, I'm following you

[12:14 PM] acemarke: oh, we might also want to have a currentSelectedThingId field in there

[12:14 PM] acemarke: maybe.

[12:14 PM] acemarke: gimme a sec to think through how the sequence for this would work

[12:14 PM] jrajav:: No prob

[12:15 PM] acemarke: so to start, we'll say the user's already logged in, and we've fetched our data, so both currentUser and things are filled up appropriately

[12:16 PM] acemarke: so our ThingList is displaying all its ThingListItems

[12:17 PM] acemarke: so I click on ThingListItem #1 to select it, and we dispatch {type : SELECT_THING, payload : {id : 1} }

[12:17 PM] acemarke: reducer copies the values from things.byId[1] over to thingBeingEdited

[12:18 PM] acemarke: and we want to pass all that down to our form over in the RightMainPanel

[12:19 PM] acemarke: so, TopLevelComponent renders <RightMainPanel thingBeingEdited={state.thingBeingEdited} />, which renders <ThingEditForm thingBeingEdited={this.props.thingBeingEdited} />

[12:19 PM] acemarke: which renders a bunch of inputs

[12:20 PM] acemarke: and I'm assuming we're going with controlled inputs here

[12:20 PM] jrajav:: Controlled inputs meaning that their values are reflected in the state?

[12:20 PM] acemarke: other way around technically - their values come from the state

[12:21 PM] acemarke: "uncontrolled" inputs means you let the user manipulate them at will, the value is stored by the browser itself per normal, and at some point in time (like form submit) you ask the actual input element itself for its value

[12:22 PM] acemarke: "controlled" inputs means you always specify a "value" prop for every input, forcing it to use that value

[12:22 PM] acemarke: which means you must also specify an onChange handler, look at the new proposed value in the event, put that in state somewhere, and re-render with the newly accepted modified value

[12:24 PM] acemarke: so. now I put my cursor in the "Thing Name" input field, which currently has a value of "Thing #1", and start typing

[12:24 PM] acemarke: and I just try to add "asdf" to the end of "Thing #1"

[12:25 PM] acemarke: each keystroke will trigger the onChange handler I've attached to the input

[12:25 PM] acemarke: and we'll assume for purposes of the scenario that I'm storing all this still in Redux state, not component state

[12:26 PM] acemarke: so the onChange handler dispatches {type : EDIT_THING_FIELD, payload : {name : "Thing #1a"} } for the first keystroke

[12:27 PM] acemarke: the thingBeingEdited reducer will update the thingBeingEdited.name field with the new value

[12:27 PM] acemarke: so thingBeingEdited.name, thingBeingEdited, and state are all new references

[12:27 PM] acemarke: scenario still clear so far?

[12:28 PM] acemarke: (again, making this all up off the top of my head, so hopefully not screwing things up :) )

[12:28 PM] jrajav:: Yes, clear so far

[12:28 PM] jrajav:: And I'm starting to see where you're heading, too ??

[12:28 PM] acemarke: so. TopLevelComponent gets the new state reference

[12:28 PM] acemarke: and will re-render:

[12:30 PM] acemarke:

render() {
    const {state} = this.props;
    return (
        <div>
           <LeftSidebar currentUser={state.currentUser} things={state.things} selectedThingId={state.selectedThingId} />
           <RightMainPanel thingBeingEdited={state.thingBeingEdited} />
        </div>
    )
}

[12:30 PM] acemarke: RightMainPanel, ThingEditForm, and the "name" input obviously re-render

[12:31 PM] acemarke: the LeftSidebar will attempt to re-render

[12:31 PM] acemarke: so at this point, the question is just how many SCU implementations you've slapped on the rest of your components

[12:32 PM] acemarke: in theory, LeftSidebar is a purely presentational component

[12:32 PM] acemarke: all it really cares about is rendering CurrentUserDetails and ThingList

[12:32 PM] jrajav:: Aren't they both purely presentational if we're using controlled inputs?

[12:32 PM] acemarke: ?

[12:33 PM] jrajav:: RightMainPanel as well I mean

[12:34 PM] acemarke: yeah, sure. the semi-point of the scenario, I guess, is that the state change really only "affects" one small input, buried deep down inside RightMainPanel

[12:34 PM] acemarke: but, because we're doing things purely top-down, every keystroke is going to try to re-render LeftSidebar

[12:34 PM] jrajav:: Right

[12:34 PM] acemarke: now, maybe we did hook up an SCU implementation for LeftSidebar

[12:35 PM] acemarke: but that's still work that doesn't have to be done

[12:35 PM] jrajav:: Well

[12:35 PM] jrajav:: If everything really is a pure dumb component

[12:35 PM] jrajav:: And we use Immutable.js for the full state tree

[12:35 PM] jrajav:: Then wrap every dumb component in a recompose pure()

[12:35 PM] jrajav:: It's pretty much all done for us, isn't it?

[12:35 PM] jrajav:: Without writing any extra code (except the pure() call), LeftMainSidebar would SCU correctly and not re-render

[12:36 PM] acemarke: if you really do do that for every component, mostly, yeah

[12:36 PM] acemarke: so let's invert the scenario a bit

[12:36 PM] acemarke: let's say that TopLevelComponent, LeftSidebar, and RightMainPanel are 100% presentational components, no connections at all

[12:36 PM] acemarke: CurrentUserDetails, ThingsList, and ThingEditForm are all connected

[12:37 PM] acemarke: and each one has a simple mapStateToProps that is just grabbing their appropriate top-level chunk of state

[12:37 PM] acemarke: once again, I place my cursor in the "Name" field after "Thing #1" and type "a"

[12:38 PM] acemarke: edit action dispatches, thingBeingEdited.name is updated, store notifies subscribers

[12:38 PM] acemarke: CurrentUserDetails will re-run mapStateToProps and return {currentUser : state.currentUser}

[12:39 PM] acemarke: connect() will shallow-compare that return value vs the last return value, see things haven't changed, and skip re-rendering

[12:39 PM] acemarke: ditto for ThingsList

[12:39 PM] acemarke: ThingEditForm will see that thingBeingEdited has changed and re-render

[12:39 PM] acemarke: but TopLevelComponent, LeftSidebar, and RightMainPanel never even had to do anything

[12:40 PM] acemarke: they do not know, and they do not care, that anything happened with the state at all

[12:40 PM] jrajav:: I'm definitely seeing the point here, but - are we not just trading one linear set of compare operations for another?

[12:40 PM] jrajav:: I do see the argument that the connect()'ed set may be smaller

[12:41 PM] jrajav:: But if the state tree is organized sensibly w.r.t. the UI they may not be that dissimilar

[12:41 PM] acemarke: yeah, I'm not saying that the overall intent and behavior are drastically different

[12:42 PM] jrajav:: In this case specifically they're almost identical - we seem to be running three comparison funcs outside of the "real" re-render (the form) in either case

[12:42 PM] acemarke: but depending on how nested your UI is, and how much data you've got, you could certainly be cutting out a number of intermediate layers

[12:43 PM] jrajav:: But you're trading those intermediate layers for however many container components you have

[12:43 PM] acemarke: it also is, as you said, a question of whether or not you want to explicitly tag all your more layout-y components with SCU

[12:43 PM] acemarke: and deal with the props management

[12:45 PM] acemarke: for me personally, I would rather do a lot less typing, get the same or better performance, and have less data flow to track

12:45 PM] jrajav:: But it's not explicit if we rely on recompose's pure() implementing it for us

[12:46 PM] jrajav:: You do have to remember to do that though...

[12:47 PM] acemarke: yeah, that's what I meant. just having to actually include it in each component definition

[12:47 PM] jrajav:: Right

[12:48 PM] acemarke: so lemme just give a general description of my own app, which is obviously an influence on my thinking

[12:48 PM] acemarke: rewriting the client for an app I built a few years back in GWT

[12:50 PM] acemarke: basically a map planning tool that uses a 3D globe, lets the user interact with the globe. left pane has some control buttons and a treeview with all items in the project. bottom pane has tabs to show attribute forms for each different data type in the tree. data items are shown in the tree, can be viewed and edited in the forms, and are displayed on the globe.

[12:50 PM] acemarke: visually, the app has a marked resemblance to Google Earth Desktop's layout: http://elmcip.net/sites/default/files/platform_images/launch_google_earth.jpg

12:50 PM] acemarke: bump up the globe and put a tabbed section with forms inside in the bottom third of the right pane

[12:52 PM] acemarke: so for my particular app, I've got some pretty heavy nesting inside that tree, need to show/edit items in the forms, and need to render them on the globe, and update all of the above on selection, editing, etc

[12:52 PM] acemarke: my top couple layers are very much purely presentational and dumb

[12:52 PM] acemarke: my TreeView is itself also presentational and dumb at the top, but displays a "ConnectedFolder" tree item for each data type

[12:53 PM] acemarke: the individual nested tree items then follow that "grab data for self, pass just ID to children" paradigm

[12:53 PM] acemarke: and only render their children if expanded

12:54 PM] acemarke: so, there's some pretty serious nesting going on, but if I do something like click on a tree item to make it the currently selected thing, only that tree item, the appropriate attributes form, and the corresponding individual globe display component will really update

12:55 PM] acemarke: I don't have my tree item components optimized to use per-component-instance memoized selectors yet, but so far performance hasn't been an issue. always something I can throw in later.

[12:56 PM] acemarke: so, that sort of structure is obviously an influence on my thought process.

12:58 PM] acemarke: as well as Dan Abramov's recent optimization pass at the MobX vs Redux benchmark: https://twitter.com/dan_abramov/status/720219615041859584 , https://github.com/mweststrate/redux-todomvc/pulls?q=is%3Apr+is%3Aclosed

Dan Abramov@dan_abramov

Red is unoptimized Redux app. Orange is an optimized one. Green is MobX. Takeaway: in Redux, state shape means a lot https://t.co/qq3NlzbQpv

GitHub

mweststrate/redux-todomvc

redux-todomvc - Redux todoMVC, used to do some benchmarking

[12:59 PM] acemarke: and finally, a great presentation on optimizing Redux that demonstrates the various approaches to connect: http://somebody32.github.io/high-performance-redux/

[1:00 PM] acemarke: so. all that said: I personally would find the pure top-down approach annoying and verbose. BUT, if it does work better for you mentally, go for it.

[1:11 PM] jrajav:: I feel like I understand the situation a lot better now @acemarke, thanks again for the very detailed discussion

[1:12 PM] acemarke: sure. Wall O' Texts my specialty! :)

[1:13 PM] jrajav:: I think I would still like to try this current application in the purely-functional style, out of curiosity more than anything else. Perhaps there are performance concerns but this application isn't too performance bound. It does, however, have a rather large state tree, so I'm interested to see if that can be effectively organized with subproperty destructuring at each level of the tree, and just how messy it would really get

[1:14 PM] acemarke: I guess I never did ask: what sort of app do you have, and what's the data like?

[1:15 PM] jrajav:: A relatively simple search-and-display app

[1:15 PM] jrajav:: Main page will be a search view, with advanced features like a facet filtering sidebar, autocomplete, pagination, term highlighting

[1:16 PM] jrajav:: Then the search results will link to a large details view for each item, with a few dozen mostly static detail properties, spread into different tabs and views

[1:18 PM] jrajav:: So the state tree will mostly be a 'search' object with the query and facets, a 'searchResults' object (which I'm considering nesting in 'search'), then a separate 'details' object with the single selected item once you route to that

[1:19 PM] jrajav:: It's probably too simple of an app to really run into big issues with state management or performance, but it is complex enough to get a good sense for the organization and passing down properties at least

[1:22 PM] acemarke: gotcha. so, not entirely dissimilar from my hypothetical ThingEditor example, then :)

[1:22 PM] acemarke: and yeah, doesn't sound overly perf-intensive

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