Skip to content
This repository has been archived by the owner on Sep 10, 2022. It is now read-only.

RFC: Update middleware #182

Closed
wants to merge 9 commits into from
Closed

RFC: Update middleware #182

wants to merge 9 commits into from

Conversation

acdlite
Copy link
Owner

@acdlite acdlite commented May 21, 2016

As mapPropsStream shows, many (most?) higher-order components can be described in terms of a props stream transformation. If multiple such higher-order components are composed together, we reduce the number of components by composing the transformations together instead, which should be more lightweight than sending props through React's update system at each level. In other words, the following two forms are equivalent:

// Wraps the base component in three container components
compose(
  mapPropsStream(transformA),
  mapPropsStream(transformB),
  mapPropsStream(transformC)
)(BaseComponent)

// Same as this, which wraps the base component in a single
// container component
mapPropsStream(
  compose(transformA, transformB, transformC)
)(BaseComponent)

But we don't want to require that Recompose users include an observable pollyfill. Instead, I'm proposing a lightweight abstraction called "update middleware" which will serve the same purpose.

So far, I've implemented withState and withHandlers using this pattern.

If we go in this direction, we will modify the compose function to detect HOCs defined using middleware and compose them using the second strategy from the example above.

As mapPropsStream shows, many (most?) higher-order components can
be described in terms of a props stream transformation. If multiple
such higher-order components are composed together, we reduce the
number of components by composing the transformations together
instead, which should be more lightweight than sending props
through React's update system at each level. In other words, the
following two forms are equivalent:

```js
// Wraps the base component in three container components
compose(
  mapPropsStream(transformA)
  mapPropsStream(transformB)
  mapPropsStream(transformC)
)(BaseComponent)

// Same as this, which wraps the base component in a single
// container component
mapPropsStream(
  compose(transformA, transformB, transformC)
)(BaseComponent)
```

But we don't want to require that Recompose users include an
observable pollyfill. Instead, I'm proposing a lightweight
abstraction called "update middleware" which will serve the
same purpose.

So far, I've implemented withState and withHandlers using
this pattern.

If we go in this direction, we will modify the compose function to
detect HOCs defined using middleware and compose them using the
second strategy from the example above.
@acdlite
Copy link
Owner Author

acdlite commented May 21, 2016

@wuct @istarkov

mapProps(props => ({
...omit(props, [propName]),
...props[propName]
}))
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In case it's not clear why this is included in this PR (other than being the right way to define flattenProp, anyway), the idea is that there are some helpers that we don't want to define using middleware, because middleware necessitates using a class component, even when a functional component would suffice. What we really want is to use a functional component unless the HOC is composed with another HOC which must use a class. Put another way, a single functional component is better than a class component, but a single class component is better than a class component and a functional component.

To achieve this, we will special case mapProps to use a functional component by default, but use middleware when being composed with other middleware-using HOCs.

@acdlite
Copy link
Owner Author

acdlite commented May 21, 2016

Also, to be clear, this abstraction would be private: it would not be exposed to the public, except to advertise as a performance optimization.

if (shouldMap(this.props, nextProps)) {
this.computedProps = propsMapper(nextProps)
}
const update = (props, cb) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to create a update function here.
We can move next.update(...) to line 31.

@wuct
Copy link
Contributor

wuct commented May 23, 2016

I like this new middleware feature. I believe the new compose function could also solve #97 😄

@acdlite
Copy link
Owner Author

acdlite commented May 23, 2016

Yeah, exactly. The whole point is to solve #97.

@acdlite acdlite mentioned this pull request May 23, 2016
@acdlite
Copy link
Owner Author

acdlite commented May 24, 2016

This is working out better than I thought. Turns out we don't even need a smart compose:

if (BaseComponent[MIDDLEWARE_INFO]) {
return createComponentFromMiddleware(
middlewares.concat(BaseComponent[MIDDLEWARE_INFO].middlewares),
BaseComponent[MIDDLEWARE_INFO].BaseComponent
)
}

destroy() {
this.xforms.forEach(xform => {
if (typeof xform.destroy === 'function') {
xform.destroy()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the only need the middleware returns object is .destroy?

@istarkov
Copy link
Contributor

istarkov commented Jun 7, 2016

Just investigated a little, and looks like rxjs 5 does not give a lot overhead on dist size,
here is an example of mapProps, withState, withHandlers based on rxjs, the result distributive size is 28kb minified, 7kb zipped.

And yes all this functions is written over props$ stream so should be used like

mapPropsStream(flow(
     mapProps(...), 
     withState(...) 
))(BaseComponent)

example

@acdlite
Copy link
Owner Author

acdlite commented Jun 7, 2016

But what about the cost of importing a single module? I want to keep that as small as reasonably possible so that library authors can rely on individual helpers guilt free.

@acdlite
Copy link
Owner Author

acdlite commented Jul 5, 2016

Just an update: this PR is on hold pending further investigation. It's not clear to me there's any performance benefit to squashing components this way.

If/when it can be proven that there is a significant performance boost, we will reevaluate.

@wuct
Copy link
Contributor

wuct commented Jul 6, 2016

We should open an issue for adding performance testing

@rtsao
Copy link

rtsao commented Jul 12, 2016

There's still a benefit in terms of usability so even if there was no performance benefit I think it would still be worthwhile.

  • Devtools ballooning
  • Needing traverse through several wrappers when accessing refs or instance methods of a component

@istarkov istarkov mentioned this pull request Sep 16, 2016
@wuct wuct mentioned this pull request Dec 13, 2016
@gregberge
Copy link

This is the approach used in recompact. I did some benchmarks, you can find it in the repo. Some conclusions from benchmarks:

  • At mount it is better only if you stack a lot of hoc
  • At update it's always better, event with a small number of hoc
  • Performance are never degraded in comparison of recompose

However there is a true benefit in devtools.

@jeffbski
Copy link

@neoziro Thanks for mentioning your project. It would be nice if you have a way to view the benchmark results without having to install and run them from your repo. Also include the summary like you have provided here.

@cvle
Copy link

cvle commented Feb 1, 2017

Updated Benchmarks with interesting results.

@gregberge
Copy link

The approach of reassemble is different, you are not exposing any hoc and it's not a drop-in replacement for recompose even if the API seems the same.

Also you replace compose that is like a native approach is composition, users should be able to write:

pure(withProps({ foo: 'bar' })(Component))
// or 
compose(
  pure,
  withProps({ foo: 'bar' }),
)(Component)

You lose the composition aspect of hoc. It reminds me of mixout.

Also I updated recompact benchmarks and published the results (removing enzyme was the solution to stabilize them). My results are very different from yours:

@cvle
Copy link

cvle commented Feb 1, 2017

@neoziro As I mentioned reassemble is not using HOCs. I changed the project description to be more clear. I'm still puzzled by the differences in our two benchmarks. I can't spot the cause, anyone?

I like the idea of recompact, in fact I considered a similar implementation. Atm I'm using reassemble in some of my projects and I believe initial render has almost cut in half . Though I need to do more performance testing. I'll keep an eye on recompact.

@cvle
Copy link

cvle commented Feb 1, 2017

@neoziro Your benchmark is incorrect. You need to cleanup on each run, otherwise the React reconciler kicks in and prevent any rerendering.

@cvle
Copy link

cvle commented Feb 1, 2017

Another difference between recompact and reassemble is that recompact cannot squash context related HOCs and the Lifecycle HOC. I often use lifecycle hooks for deferred rendering and to deal with animations (e.g. in my other open source project: react-css-transition).

@gregberge
Copy link

@cvle you are right, I will update benchmarks soon.

@SimonM0
Copy link

SimonM0 commented Jul 13, 2017

@acdlite Will this be PR be merged in soon?

@istarkov
Copy link
Contributor

@fried-chicken there are no reason for this now. The only issue is a React dev tools tree length.
There are no big performance gain as vdom works really fast when you have a pack of components with only one child. May be some real life (not a tests like lets compare 10 plain withState perf vs 10 recompose withState) exists but we have not seen them.
If you really need (really?) this you can use https://github.com/neoziro/recompact as drop in replacement for recompose or similar solutions.

@istarkov
Copy link
Contributor

@fried-chicken benchmarks https://github.com/neoziro/recompact/tree/master/src/__benchmarks__

@atomanyih
Copy link

@istarkov
If the work has already been done, it doesn't make things slower, and it makes it easier for people to develop with recompose, why not include it?

I love recompose and use it frequently, but it can make debugging devtools kind of incomprehensible. This is especially true for other people on a team who didn't write the recomposed components and have to crawl through the component tree to try to figure out which file contains one particular occurrence of withProps(withProps(withProps(UiComponent)))

@istarkov
Copy link
Contributor

I dont use devtools so this is not a problem for me. And I love recompose because of simplicity of its code, IMO its so simple that does not need a docs. This change will make code less readable. So for me there are no advantages to apply this.

@smeijer
Copy link

smeijer commented Sep 20, 2017

I dont use devtools so this is not a problem for me.... So for me there are no advantages to apply this.

Sorry, but from a maintainer of a project with 7.000 stars on github, and ~ 780.000 downloads each month from npm, this response feels kind of wrong.

Don't get me wrong. recompose is an awesome library, and the amount of stars and downloads support my statement. I do understand the "my project", "free time" aspect and everything. But the ballooning is a problem.

The only reason for me to not switch over to recompact or reassemble; is that this project seems to be more alive / maintained.

@istarkov
Copy link
Contributor

How amount of stars or downloads can affect decisions? It's impossible to make project good for all so a metric "good for me" is very very good. If we'll start to accept every PR and every suggestion here it will be unusable project.

@rbong
Copy link

rbong commented Sep 25, 2017

The issue of devtools has been brought up, but not the one of serialization, particularly for snapshot tests with enzyme and Jest.

When the component tree is so polluted, snapshot tests are going to change often, and the change diffs are not going to be readable. This makes snapshot testing too volatile to use in many cases. I know that this seems like a pretty specific use case, but 500,000+ people have downloaded enzyme-to-json in the past month.

Are there no workarounds, or am I wrong?

@istarkov
Copy link
Contributor

@rbong have you checked your words? Have you seen the snapshot generated by enzyme 2 json?
If yes please answer what are you see in snapshot?

@gregberge
Copy link

@rbong I think it is better to let recompose a simple toolkit of hoc utilities (as simple as possible). If you want to use it as a framework and stack several hocs you should take a look to recompact. I am not very active on it lately, but it is still maintained and I am open to PR.

@istarkov
Copy link
Contributor

@neoziro the @rbong problem is not exist, its obvious if you just look into the snapshot. As snapshot is not the vdom tree, and independent of amount of hocs used.

@gregberge
Copy link

@istarkov I mean for dev tools or other issues. You should lock this conversation.

@deepsweet
Copy link
Contributor

I think it's a matter of creating a special 3rd party Babel plugin to convert compose() and base HOCs to something else (if it's possible at all ofc).

@smeijer
Copy link

smeijer commented Sep 25, 2017

I think such a plugin would be awesome. But I'm not sure if it would work. This because recompact and reassemble work differently when you need something like redux.

In this snippet, dispatch is undefined in both recompact and reassemble, but it does work in recompose.

export default compose(
  connect(),
  withHanders({
    onClick: ({ dispatch }) => () => {
      dispatch('foo');
    },
  }),
)

@istarkov istarkov closed this Sep 25, 2017
@rbong
Copy link

rbong commented Sep 25, 2017

@istarkov my apologies, I did not know enzyme-to-json contained a function to only render DOM nodes.

@istarkov
Copy link
Contributor

@rbong not a problem :-) no need in apologies

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

Successfully merging this pull request may close these issues.

None yet