Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Redux + React with only stateless functions #1176

Closed
aunz opened this issue Dec 23, 2015 · 9 comments
Closed

Question: Redux + React with only stateless functions #1176

aunz opened this issue Dec 23, 2015 · 9 comments

Comments

@aunz
Copy link

aunz commented Dec 23, 2015

Redux is awesome!

I am wondering if this is a good idea: use Redux at the top, with store.subscribe to listen to store.dispatch, then aggregate result with store.getState() and finally call ReactDom.render. All the Reacts components are written only using stateless functions.

//React components
function ParentComponent(props) {
  return <div>
   Hello World
    <Child1 {...props} />
    <Child2 {...props} />
    {props.state.someCondition ? <Child3 /> : null}
  </div>
}

function Child1(props) {
  return <div>
    Hi from Child 1 {props.state.someValue}
  </div>
}

function Child2(props) {
  function handleClick(event) {
    props.dispatch({
      type: 'SOME_ACTION',
      payload: {event: event}
    })
  }
  return <div onClick={handleClick}>
    Hi from Child 2 {props.state.someOtherValue}
  </div>
}

function Child3(props) {
  return <div>
    Hi from Child 3
    <GrandChild1 {...props}/>
  </div>
}

function GrandChild1(props) {
  function handleClick(event) {
     dispatch({type: 'FETCHING_STUFF'})
     fetch('someStuff').then(r => {
        dispatch({type: 'FETCHED_STUFF', fetchedStuff: r})
     })
  }
  return <div onClick={handleClick}>
    {props.state.fetching ? <div>Spinning</div> : null}
    Hello from GrandChild 1
    {props.state.fetchedStuff ? <div>{props.state.fetchedStuff}</div> : null}
  </div>
}

//redux 
const store = createStore(reducer,initialState)

store.subscribe(() => {
  const props = {
     state: store.getState(),
     dispatch: store.dispatch
  }

  ReactDom.render(<Parent {...props}, mountNode)
})

So each time a store.dispatch is called, ReactDom will render the whole tree again. Essentially, all the components are "dumb", they get the props from Redux store.getState().

What are the pros and cons of this usage?

My limited understanding is that "stateless components can follow a faster code path within the React core" and will be further optimised in the future so performance will not be an issue even without the Lifecycle API such as shouldComponentUpdate. Is this correct?

Since Redux's principle is not to mutate the state, so calling ReactDom.render each time with a new state (in contrast to using this.setState({}) insides a React Component) is a better fit with the concept or principle of immutability?

The other advantage is that the app can be written without using this, without class nor extends, all pure functions

Thanks a lot!

@markerikson
Copy link
Contributor

I'm... pretty sure that's wrong on several levels.

First, my understanding is that calling ReactDOM.render() repeatedly starts everything over, rather than taking the existing tree of components and updating it. Second, the comment on stateless components is a POSSIBLE future optimization, not one that exists now. You're reading WAY too much into that.

@gaearon
Copy link
Contributor

gaearon commented Dec 24, 2015

It's a fine approach for learning but I don't think it scales well.

  1. Functional components aren't fast now. They might get optimizations in the future but it's just not the case in the next few releases. So expect them to be like regular components.
  2. connect() from React Redux does a ton of trickery to be very optimized. Take a look at its source. It tries hard to be fast whereas functional components don't try at all.
  3. Using connect() is also a perf win because always re-rendering from the top means you're doing a bunch of unnecessary reconciliation. Re-rendering on every change is impractical unless all your component have very strict shouldComponentUpdate() implementations like those provided in Om.
  4. When you always render from the top you are coupling parent components too hard to what child components need to render. You're essentially passing many props that are only required by children, and changing them can involve painful refactorings.

Instead as soon as you see that component passes props down without using it, we suggest generating a "container" component using connect().

In my Egghead video course (linked from README) I actually build an app rendering from the top, and later introduce container components precisely to decrease this coupling. These videos contain instructions on when it's best to do this.

@gaearon gaearon closed this as completed Dec 24, 2015
@gaearon
Copy link
Contributor

gaearon commented Dec 24, 2015

First, my understanding is that calling ReactDOM.render() repeatedly starts everything over, rather than taking the existing tree of components and updating it

No, it reconciles the existing tree, just like calling setState. But it's still slower than setState somewhere in the middle because it has more to reconcile.

@aunz
Copy link
Author

aunz commented Dec 24, 2015

👍

@slorber
Copy link
Contributor

slorber commented Dec 24, 2015

Hey,

Because JS is not immutable by default, it has been decided that the functional components do not optimize with something akin to PureRenderMixin by default (but we may be able to in the future).
So your pure functional components will generally be slower than regular classes with an appropriate shouldComponentUpdate method. This has surprised me as well. React issue

I have been running an app for 1.5 years in production. We built a framework that looks quite similar to Redux, even before Flux came out, but with a badly designed API and also using some cursors (we will migrate to redux or ELM at some point). We don't have connect or things like that, we always render from the very top exactly like you propose. You can read more here
Even on text inputs, on every keystroke, we don't use this.setState but actually renders from the very top a new state. You can see this in this video

Here are some feedbacks:

  • Rendering from the very top makes the text inputs not very responsive if you have to re-render everything everytime, even if you have something like PureRenderMixin on ALL your components like we have. If your text input is very deeply nested, you still have to re-render all the intermediate components, all the wrappers / HOC that provide react context to the childs etc... This takes time on every keystroke. It does not seem like a big deal on fast computers but it is clearly a problem for us on slow mobile devices. We are moving back to setState for performance reasons for very local state like text inputs, .hover classes etc...
  • Rendering from the very top means you always have to dispatch the props from the global state to the childs. If you need a new prop that is in a very deeply nested child of your tree, you may have to modify a LOT of intermediate components. And you end up with a lot of intermediate components that pass a lot of props to their childs without even using it... Really it complicates the maintenance of the application over time.

Just to illustrate the mess it created for us, here is our real legacy layout top-level component. Over time it has become a real mess, and just imagine that on every text input keystroke it has to re-render :D layout.jsx

#fail


Where we are moving now: having layout components and context providers at the root of the tree.
Generally these don't change often:

  • the layout may change according to window size breakpoints (ie window resize events)
  • the react context change when the user language changes or the user connect/disconnect...

In most cases, you don't need to re-render these components when your state change.
However there are some cases when the layout still need to change according to state, like when you have a property "menuFolded": you can get it with connect

Then on your layout you put widgets. A widget is an autonomous component that has its own reducer, use something like "connect" to get the state of that reducer. The widget only re-renders when its state changes. I try to make the widget only receive its own state, to make it really independant but it is really a mater of taste and many don't adopt a similar approach and assume the widget should have a global knowledge of the global appstate to select the state it needs to use.

@aunz
Copy link
Author

aunz commented Jan 1, 2016

@slorber thanks a lot! Very helpful! Have a 🍵

@suni-masuno
Copy link

suni-masuno commented Mar 7, 2017

Some current real world feedback to this pattern

So we're using this same basic layout in a production application, so I can provide a little 'real world' feedback. About 10 months into dev and 6 months into production at this point. Sorry for the thread necromancy, but I felt the need to add to this.

Not Pros or Cons (for us right now)

First, we aren't dealing with any efficiency/performance issues at the moment. We're using seamless-immutable and thinking a fair bit about the draw cycle (react render step) during our design and coding, but we've found that system performance is still high enough that it hasn't yet been a meaningful limit for us. Even on old phones.

We're looking at purity as a way to get more efficient, but since we're not struggling there that keeps dropping in priority.

We aren't using time travel. It looks cool and all... we just haven't found a use case for it yet.

Pros

We got into prod real fast. I think that's more to modern JS in general, but it was still a big win with us.

When 100% of the dom is derived from the state at the moment it's pretty cool/fun. Time travel is really fun to watch, but also useful for things like debugging. Grabbing a snapshot of the state to deal with problems has become something of a standard for us, and a pleasant one at that.

Trapping everything meaningful in a dispatch to the same state makes user behavior easy to watch. For our purposes that means it is easy to audit for legal reasons, but it may have other uses for other teams.

It's so testable! That's really what motivated us to try this arrangement, and it hasn't disappointed. I cannot stress enough how wonderful this is. For me it justifies everything else.

It's so compartmental. When we work on reducers, actions, or components we don't feel much, if any, coupling with the others. Mentally this feels, at least right now, like it makes it "easier to code" at least for us.

Cons

It's hard to onboard and train. This architecture is so unintuitive to new CS grads and even to more experienced developers from other worlds (in our case java) that it is really affecting our onboarding.

It breaks compatibility with a lot of libraries. Much of the React ecosystem requires at least one stateful component.

Overall

We're loving it, but it feels like the bleeding edge and has a few related drawbacks. There are also a lot of unanswered questions about what will happen to the ecosystem in the long run.

@esr360
Copy link

esr360 commented Jul 20, 2018

@suni-masuno I would be very curious to hear your feedback on this some 16 months later. Do you still agree with all of your pros and cons? What has changed in this time?

@joegesualdo
Copy link

@suni-masuno I’m also very interested to hear if your pros and cons have changed and if it still feels like the bleeding edge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants