reduxjs / redux

A JS library for predictable global state management
https://redux.js.org
MIT License
60.85k stars 15.27k forks source link

Question: Redux + React with only stateless functions #1176

Closed aunz closed 8 years ago

aunz commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

:+1:

slorber commented 8 years ago

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:

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:

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 commented 8 years ago

@slorber thanks a lot! Very helpful! Have a :tea:

suni-masuno commented 7 years ago

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 commented 6 years ago

@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 commented 6 years ago

@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.