Closed adrianhelvik closed 6 years ago
Note that we're actively working on fixing context so that we can embrace it as a supported API. :-) https://github.com/reactjs/rfcs/pull/2 https://github.com/facebook/react/pull/11818
I appreciate the detailed writeup but overall I think this proposal is counter to how we think about React, and I don’t really see this happening.
We are fixing context so at least that part of the motivation is solved.
While using systems like MobX that wrap all the data structures is possible in React, we don't necessarily want to encourage this style of programming, and I don't think making it even more automatic is something we'd want to do.
I think the biggest drawback of this proposal is that it's both implicit and very powerful. We try not to combine those. The only implicit API we have is context, and even that only affects data and not behavior. It's also opt-in and passes the grep test. The middleware API can't pass it because it affects all components indirectly. This essentially changes the contract between components, saying "here are your props, unless there's middleware on the stack, in which case who knows what you'll really get as props". That defies any potential optimizations React could make, including compilation techniques we're currently exploring.
Totally understand that!
This is an incomplete draft for a feature I think could be really cool. It can replace higher order components and context in a way I think is more in the component spirit of React.
I do not know if this feature is feasible or desirable for React, especially as it would lead to a bigger API surface. The proposal is written as if it was documentation to give a feel for how it would be to use it.
About React middleware
A middleware is applied somewhere in the component tree and are instantiated just after child components are instantiated and just before they mount. In this context, child components means child components at any depth.
Middleware is used just like normal components, but it works slightly differently. When a middleware element is used it added to the middleware stack. If it is already on the middleware stack, it removed from the stack and pushed to the end, with the most innermost props.
Simplified example
In addition to the actual classes, the stack also includes its most recent props. But this is roughly how it works.
Lifecycle methods
Additions to the existing lifecycle methods
Mounting
Unmounting
static shouldMiddlewareMount(ReactComponent)
Determine if the current middleware should apply for a component. If the method isn't implemented, the middleware will always be applied.
If a middleware is on the middleware stack, this method is called every time a component is constructed.
Example
static shouldMiddlewarePropagate(ReactComponent)
Determine whether the middleware should remain on the middleware stack or be excluded for the subtree below the given component. If not specified it returns false, in other words: The default behavior for middleware is to propagate.
This is useful if you want to limit middleware from affecting deeply nested children. It is also useful for only giving middleware access to its immediate children.
Example
middlewareWillMount(reactInstance)
Called before the child component calls componentWillMount. This is a good place to initialize state for the middleware instance.
MiddlewareWillUnmount(reactInstance)
Called before the child component calls componentWillUnmount.
Example
This is a naïve example of how it could be used to trigger automatic updates with Mobx.
interceptRender(children)
interceptRender is called with the result from the render function of the component. The resulting value is what is used to render the DOM.
Example
This is an example of a middleware that transforms object classes into a string. The result works similarly to how ng-class works in AngularJS.
Why middleware?
React middleware can replace two problematic patterns used with React.
Context
The h2 on context in the React docs says "Why Not To Use Context". Context is however a very useful feature. And people have been and will continue to use and abuse it in the forseeable future. React Router has started abusing context in its most recent version, which shows that there is clearly a need here.
With middleware, as I propose it, you would be able to inject props into an arbitrary subtree of your app. This has performance implications, but would be an ideal scenario for libraries such as React Router, as the relevant props (or as it is now, context) rarely changes. With middleware shouldComponentUpdate will still function like you would expect.
Higher order components
A primal rule of programming is DRY. When using Mobx with React, you must use the observer decorator on all reactive classes. This isn't a really big deal, but not having to include that would reduce the size of every single observer component by two lines and most importantly, I wouldn't forget it.
When creating a higher order component static properties are no longer available. The package hoist-non-react-static is designed so that you should be able to access static properties of higher order components transparently. If a static property is initialized in the lifecycle methods of a component, it will however not be proxied.
Creating higher order components is also a messy affair.
With middleware you could achive the same thing in a React way. To replace connect from react-redux you could set shouldMiddlewarePropagate to return false, and it would affect only one component.
Alternatively you could use static properties for mapStateToProps and mapDispatchToProps.