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

Feature request: middleware #12085

Closed
adrianhelvik opened this issue Jan 23, 2018 · 3 comments
Closed

Feature request: middleware #12085

adrianhelvik opened this issue Jan 23, 2018 · 3 comments

Comments

@adrianhelvik
Copy link

adrianhelvik commented Jan 23, 2018

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.

<MiddlewareA>
  {/* middleware stack for "A": [MiddlewareA] */}
  <A />
  <MiddlewareB>
    {/* middleware stack for "B": [MiddlewareA, MiddlewareB] */}
    <B>
      <MiddlewareA
        {/* middleware stack for "C": [MiddlewareB, MiddlewareA] */}
        <C />
      </MiddlewareA>
    </B>
  </MiddlewareB>
</MiddlewareB>

Lifecycle methods

Additions to the existing lifecycle methods

Mounting

  • new: Middleware.shouldMiddlewareMount
  • new: Middleware.shouldMiddlewarePropagate
  • Component#constructor
  • new: Middleware#constructor
  • new: Middleware#middlewareWillMount()
  • Component#componentWillMount()
  • Component#render()
  • new: Middleware#interceptRender()
  • Component#componentDidMount

Unmounting

  • new: Middleware#middlewareWillUnmount

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

class TransformInlineStyles extends React.Middleware {

  /**
   * Only mount middleware when you set
   * transformInlineStyles to a truthy
   * value. Children of the given
   * component can still enable
   * the middleware
   */
  static shouldMiddlewareMount(Component) {
    return Component.transformInlineStyles
  }

  // ...
}

const A = props => (
  // ...
)

const B = props => (
  // ...
)
B.transformInlineStyles = true

const App = () => (
  <TransformInlineStyles>
    {/* Not applied to "A" */}
    <A>
      {/* Applied to "B" */}
      <B />
    </A>
  </TransformInlineStyles>
)

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

import React from 'react'

class ProvideTheme extends React.Middleware {
  static StopPropagation = props => props.children

  static shouldMiddlewarePropagate(Component) {
    return Component !== this.StopPropagation
  }

  // ...
}

const App = () => (
  <ProvideTheme>
    {/* middleware stack for "A": [ProvideTheme] */}
    <A>
      <ProvideTheme.StopPropagation>
        {/* middleware stack for "B": [] */}
        <B />
      </ProvideTheme.StopPropagation>
      {/* middleware stack for "C": [ProvideTheme] */}
      <C />
    </A>
  </ProvideTheme>
)

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.

class Observer extends React.Middleware {
  middlewareWillMount(reactInstance) {
    this.dispose = autorun(() => {

      // Let Mobx track the observables
      // used in the render method.
      reactInstance.render()

      // Force update the component instance
      // after Mobx has stopped tracking the
      // autorun function.
      //
      // .. yes, I know it's hacky.
      setTimeout(() => {
        reactInstance.forceUpdate()
      })
    })
  }

  middlewareWillUnmount(reactInstance) {
    // Stop listening for changes from Mobx
    this.dispose()
  }
}

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.

class ObjectClassNames extends React.Middleware {

  /**
   * This is a life cycle method.
   *
   * Intercept the render method and recursively
   * loop through all children, performing
   * this.transformProps() on their props.
   */
  interceptRender(children) {
    return React.Children.map(children, child => {
      if (! React.isValidElement(child)) {
        return child
      }
      return {
        ...child,
        props: this.transformProps(child.props),
        children: this.interceptRender(child.children)
      }
    })
  }

  /**
   * If a child has an object className, call
   * this.transformClassname() on it.
   */
  transformProps(props) {
    if (! props || ! props.className || typeof props.className !== 'object') {
      return props
    }

    return {
      ...props,
      className: this.transformClassname(props.className)
    }
  }

  /**
   * Concatenate the truthy keys of the className
   * object into a string.
   */
  transformClassname(className) {
    const result = []

    for (const key of Object.keys(className)) {
      if (className[key]) {
        result.push(key)
      }
    }

    return result.join(' ')
  }
}

const Widget = (props) => (
  <div className={{ 'Widget': true, 'Widget--active': props.active }}>
    Some widget
  </div>
)

const App = () => (
  <ObjectClassNames>
    <Widget active={true} />
  </ObjectClassNames>
)

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.

@gaearon
Copy link
Collaborator

gaearon commented Jan 23, 2018

Note that we're actively working on fixing context so that we can embrace it as a supported API. :-)
reactjs/rfcs#2
#11818

@gaearon
Copy link
Collaborator

gaearon commented Jan 28, 2018

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.

@gaearon gaearon closed this as completed Jan 28, 2018
@adrianhelvik
Copy link
Author

Totally understand that!

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

No branches or pull requests

2 participants