Preface
In React, components are the primary unit of code reuse, and composition-based component reuse mechanism is quite elegant. However, for finer-grained logic (state logic, behavior logic, etc.), reuse is not so easy:
Components are the primary unit of code reuse in React, but it's not always obvious how to share the state or behavior that one component encapsulates to other components that need that same state.
(Excerpted from Use HOCs For Cross-Cutting Concerns)
It's hard to break out state logic as a reusable function or component:
However, we often can't break complex components down any further because the logic is stateful and can't be extracted to a function or another component.
Because all along, there has been a lack of a simple and direct component behavior extension method:
React doesn't offer a way to "attach" reusable behavior to a component (for example, connecting it to a store).
(Excerpted from It's hard to reuse stateful logic between components)
Wait, isn't HOC an extension method, even Mixin works?
Strictly speaking, Mixin, Render Props, HOC and other solutions can only be considered upper-level patterns explored under the existing (component mechanism) game rules:
To be clear, mixins is an escape hatch to work around reusability limitations in the system. It's not idiomatic React.
(Excerpted from Proposal for porting React's Mixin APIs to a generic primitive)
HOCs are not part of the React API, per se. They are a pattern that emerges from React's compositional nature.
(Excerpted from Higher-Order Components)
There has never been a good solution from the root to solve the problem of logic reuse between components... until Hooks came on stage
P.S. Mixin seems to belong to lower-level solutions (React provides internal support), but actually only built in a mixin() utility function, the only special thing is the conflict handling strategy:
A class can use multiple mixins, but no two mixins can define the same method. Two mixins can, however, implement the same lifecycle method. In this case, each implementation will be invoked one after another.
1. Exploration
To further reuse fine-grained logic below component level (such as handling cross-cutting concerns), various solutions have been explored:
The general process is like this:
| Theoretical Basis | Solution | Defects |
|---|---|---|
| Mixin | Component complexity rises sharply, difficult to understand | |
| Declarative is better than imperative, composition is better than inheritance | Higher-Order Components, Render Props | Multiple abstractions lead to Wrapper Hell |
| Learn from functional programming ideas | Hooks | Writing restrictions, learning costs, etc. |
2. Mixin
Mixins allow code to be shared between multiple React components. They are pretty similar to mixins in Python or traits in PHP.
The emergence of Mixin solution comes from an OOP intuition. Although React itself has some functional programming flavor, to cater to user habits, early on only provided React.createClass() API to define components:
React is influenced by functional programming but it came into the field that was dominated by object-oriented libraries. It was hard for engineers both inside and outside of Facebook to give up on the patterns they were used to.
Naturally, (class) inheritance became an intuitive attempt. And in JavaScript's prototype-based extension mode, Mixin solution similar to inheritance became the first choice:
// Define Mixin
var Mixin1 = {
getMessage: function() {
return 'hello world';
}
};
var Mixin2 = {
componentDidMount: function() {
console.log('Mixin2.componentDidMount()');
}
};
// Use Mixin to enhance existing components
var MyComponent = React.createClass({
mixins: [Mixin1, Mixin2],
render: function() {
return <div>{this.getMessage()}</div>;
}
});
(Excerpted from ancient document react/docs/docs/mixins.md)
Mixin is mainly used to solve lifecycle logic and state logic reuse problems:
It tries to be smart and "merges" lifecycle hooks. If both the component and the several mixins it uses define the componentDidMount lifecycle hook, React will intelligently merge them so that each method will be called. Similarly, several mixins can contribute to the getInitialState result.
Allows extending component lifecycle from outside, especially important in Flux and other patterns:
It's absolutely necessary that any component extension mechanism has the access to the component's lifecycle.
Defects
But there are many defects:
-
Implicit dependencies exist between components and Mixins (Mixins often depend on specific methods of components, but this dependency relationship is not known when defining components)
-
Conflicts may arise between multiple Mixins (such as defining the same
statefield) -
Mixins tend to add more state, which reduces application predictability (The more state in your application, the harder it is to reason about it.), leading to dramatic complexity increase
Implicit dependencies lead to opaque dependency relationships, maintenance costs and understanding costs rise rapidly:
-
Difficult to quickly understand component behavior, need to fully understand all dependent Mixin extension behaviors, and their mutual influences
-
Component's own methods and
statefields dare not be easily deleted or modified, because it's difficult to determine if any Mixin depends on them -
Mixins are also difficult to maintain, because Mixin logic will eventually be flattened and merged together, it's hard to figure out a Mixin's inputs and outputs
Undoubtedly, these problems are fatal
Therefore, React v0.13.0 abandoned Mixin (inheritance), and turned to HOC (composition):
Idiomatic React reusable code should primarily be implemented in terms of composition and not inheritance.
Example
(Not considering problems existing in Mixin solution) From a functional perspective, Mixin can also complete extensions similar to HOC, for example:
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.forEach(clearInterval);
}
};
// Equivalent to React.createClass below React v15.5.0
var createReactClass = require('create-react-class');
var TickTock = createReactClass({
mixins: [SetIntervalMixin], // Use the mixin
getInitialState: function() {
return {seconds: 0};
},
componentDidMount: function() {
this.setInterval(this.tick, 1000); // Call a method on the mixin
},
tick: function() {
this.setState({seconds: this.state.seconds + 1});
},
render: function() {
return (
<p>
React has been running for {this.state.seconds} seconds.
</p>
);
}
});
ReactDOM.render(
<TickTock />,
document.getElementById('example')
);
(Excerpted from Mixins)
P.S. [React v15.5.0] officially deprecated React.createClass() API, moved to create-react-class, built-in Mixin also became history together, specifically see React v15.5.0
3. Higher-Order Components
After Mixin, HOC took on the heavy responsibility, becoming the recommended solution for logic reuse between components:
A higher-order component (HOC) is an advanced technique in React for reusing component logic.
But HOC is not a newcomer, it already existed in the React.createClass() era, because HOC is built on component composition mechanism, it's a completely upper-level pattern, doesn't depend on special support
Formally similar to higher-order functions, extends behavior by wrapping a layer of component:
Concretely, A higher-order component is a function that takes a component and returns a new component.
For example:
// Define higher-order component
var Enhance = ComposedComponent => class extends Component {
constructor() {
this.state = { data: null };
}
componentDidMount() {
this.setState({ data: 'Hello' });
}
render() {
return <ComposedComponent {...this.props} data={this.state.data} />;
}
};
class MyComponent {
render() {
if (!this.data) return <div>Waiting...</div>;
return <div>{this.data}</div>;
}
}
// Use higher-order component to enhance normal component, thereby achieving logic reuse
export default Enhance(MyComponent);
Theoretically, as long as a function that accepts component type parameters and returns a component is a higher-order component ((Component, ...args) => Component), but for convenience of composition, Component => Component form HOC is recommended, use [partial function application](/articles/基础语法-haskell 笔记 1/#articleHeader11) to pass other parameters, for example:
// React Redux's `connect`
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
Compare with Mixin
Under HOC mode, outer component affects inner component's state through Props, instead of directly changing its State:
Instead of managing the component's internal state, it wraps the component and passes some additional props to it.
And, for reusable state logic, this state is only maintained in the stateful higher-order component (equivalent to extending State also has component scope), there's no conflict and mutual interference problem:
// This function takes a component...
function withSubscription(WrappedComponent, selectData) {
// ...and returns another component...
return class extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
this.state = {
data: selectData(DataSource, props)
};
}
componentDidMount() {
// ... that takes care of the subscription...
DataSource.addChangeListener(this.handleChange);
}
componentWillUnmount() {
DataSource.removeChangeListener(this.handleChange);
}
handleChange() {
this.setState({
data: selectData(DataSource, this.props)
});
}
render() {
// ... and renders the wrapped component with the fresh data!
// Notice that we pass through any additional props
return <WrappedComponent data={this.state.data} {...this.props} />;
}
};
}
Most importantly, different from Mixin's flatten + merge, HOC has natural hierarchical structure (component tree structure), this decomposition greatly reduces complexity:
This way wrapper's lifecycle hooks work without any special merging behavior, by the virtue of simple component nesting!
Defects
HOC doesn't have so many fatal problems, but also exists some small defects:
-
Extensibility limitations: HOC cannot completely replace Mixin
-
Ref passing problem: Ref is blocked
-
Wrapper Hell: HOC泛滥,出现 Wrapper Hell
Extensibility Limitations
In some scenarios, Mixin can do but HOC cannot, such as PureRenderMixin:
PureRenderMixin implements shouldComponentUpdate, in which it compares the current props and state with the next ones and returns false if the equalities pass.
Because HOC cannot access child component's State from outside, and filter out unnecessary updates through shouldComponentUpdate. Therefore, after React supported ES6 Class, it provided React.PureComponent to solve this problem
Ref Passing Problem
Ref passing problem is quite annoying under layer-by-layer wrapping, function Ref can alleviate part of it (letting HOC know node creation and destruction), so later there was React.forwardRef API:
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// Assign the custom prop "forwardedRef" as a ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// Note the second param "ref" provided by React.forwardRef.
// We can pass it along to LogProps as a regular prop, e.g. "forwardedRef"
// And it can then be attached to the Component.
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
(Excerpted from Forwarding refs in higher-order components)
Wrapper Hell
No problem that can't be solved by wrapping one layer, if there is, then wrap two layers...
Wrapper Hell problem follows closely:
You will likely find a "wrapper hell" of components surrounded by layers of providers, consumers, higher-order components, render props, and other abstractions.
Multiple abstractions also increase complexity and understanding costs, this is the most critical defect, and there's no good solution under HOC mode
4. Render Props
Like HOC, Render Props is also a veteran pattern that has always existed:
The term "render prop" refers to a technique for sharing code between React components using a prop whose value is a function.
For example, extract and reuse cursor position related rendering logic, and combine reusable components with target components through Render Props pattern:
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY
});
}
render() {
return (
<div style={{ height: '100%' }} onMouseMove={this.handleMouseMove}>
{/*
Instead of providing a static representation of what <Mouse> renders,
use the `render` prop to dynamically determine what to render.
*/}
{this.props.render(this.state)}
</div>
);
}
}
That is part of component's rendering logic is provided externally through Props, the rest unchanged parts can be reused
Analogy with HOC
Technically, both are based on component composition mechanism, Render Props has the same extension capabilities as HOC
Called Render Props, doesn't mean it can only be used to reuse rendering logic:
In fact, any prop that is a function that a component uses to know what to render is technically a "render prop".
(Excerpted from Using Props Other Than render)
But indicates that under this mode, components are composed through render(), similar to establishing composition relationship through Wrapper's render() under HOC mode
Formally, the two are very similar, both will produce a layer of "Wrapper" (EComponent and RP):
// HOC definition
const HOC = Component => WrappedComponent;
// HOC usage
const Component;
const EComponent = HOC(Component);
<EComponent />
// Render Props definition
const RP = ComponentWithSpecialProps;
// Render Props usage
const Component;
<RP specialRender={() => <Component />} />
More interestingly, Render Props and HOC can even be converted to each other:
function RP2HOC(RP) {
return Component => {
return class extends React.Component {
static displayName = "RP2HOC";
render() {
return (
<RP
specialRender={renderOptions => (
<Component {...this.props} renderOptions={renderOptions} />
)}
/>
);
}
};
};
}
// Usage
const HOC = RP2HOC(RP);
const EComponent = HOC(Component);
function HOC2RP(HOC) {
const RP = class extends React.Component {
static displayName = "HOC2RP";
render() {
return this.props.specialRender();
}
};
return HOC(RP);
}
// Usage
const RP = HOC2RP(HOC);
<RP specialRender={() => <Component />} />
Online Demo: https://codesandbox.io/embed/hocandrenderprops-0v72k
P.S. View content is exactly the same, but component tree structure differs greatly:
[caption id="attachment_1950" align="alignnone" width="625"]
react hoc to render props[/caption]
Can view through React DevTools https://0v72k.codesandbox.io/
5. Hooks
HOC, Render Props, component composition, Ref passing... why is code reuse so complex?
The root cause is fine-grained code reuse should not be bundled with component reuse:
Components are more powerful, but they have to render some UI. This makes them inconvenient for sharing non-visual logic. This is how we end up with complex patterns like render props and higher-order components.
HOC, Render Props and other solutions based on component composition, are equivalent to first packaging the logic to be reused into components, then using component reuse mechanism to achieve logic reuse. Naturally limited by component reuse, thus appearing problems like extensibility limitations, Ref blocking, Wrapper Hell...
So, is there a simple and direct code reuse method?
Functions. Extracting reusable logic into functions should be the most direct, lowest cost code reuse method:
Functions seem to be a perfect mechanism for code reuse. Moving logic between functions takes the least amount of effort.
But for state logic, still need to achieve reuse through some abstraction patterns (such as Observable):
However, functions can't have local React state inside them. You can't extract behavior like "watch window size and update the state" or "animate a value over time" from a class component without restructuring your code or introducing an abstraction like Observables.
This is exactly Hooks' idea: use functions as the smallest code reuse unit, while building in some patterns to simplify state logic reuse
For example:
function MyResponsiveComponent() {
const width = useWindowWidth(); // Our custom Hook
return (
<p>Window width is {width}</p>
);
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
});
return width;
}
(Excerpted from [Making Sense of React Hooks](https://medium.com/ @dan_abramov/making-sense-of-react-hooks-fdbde8803889), online Demo see https://codesandbox.io/embed/reac-conf-2018-dan-abramov-hooks-example-mess-around-o5zcu)
Declarative state logic (const width = useWindowWidth()), semantics are very natural
Compare with Other Solutions
Compared with other solutions mentioned above, Hooks makes component internal logic reuse no longer bundled with component reuse, truly attempting to solve (between components) fine-grained logic reuse problem from the lower level
Additionally, this declarative logic reuse solution extends React's explicit data flow and composition philosophy between components further into components, fitting React philosophy:
Hooks apply the React philosophy (explicit data flow and composition) inside a component, rather than just between the components.
Defects
Hooks is not perfect either, just for now, its shortcomings are as follows:
-
Additional learning costs (confusion between Functional Component and Class Component)
-
Has restrictions on writing (cannot appear in conditions, loops), and writing restrictions increase refactoring costs
-
Breaks performance optimization effects of
PureComponent,React.memoshallow comparison (to get latestpropsandstate, need to recreate event handler functions everyrender()) -
May reference old
state,propsvalues in closure scenarios -
Internal implementation is not intuitive (relies on a mutable global state, no longer so "pure")
-
React.memocannot completely replaceshouldComponentUpdate(because can't get state change, only targets props change) -
useStateAPI design is not quite perfect
(Excerpted from Drawbacks)
References
-
[Mixins Are Dead. Long Live Composition](https://medium.com/ @dan_abramov/mixins-are-dead-long-live-higher-order-components-94a0d2f9e750)
-
[Making Sense of React Hooks](https://medium.com/ @dan_abramov/making-sense-of-react-hooks-fdbde8803889)
No comments yet. Be the first to share your thoughts.