当前位置:网站首页>React design pattern: in depth understanding of react & Redux principle
React design pattern: in depth understanding of react & Redux principle
2020-11-06 01:23:00 【:::::::】
Original address This article belongs to the author React Introduction and best practices series , Recommended reading GUI Ten years of evolution of application architecture :MVC,MVP,MVVM,Unidirectional,Clean
Communication
React A great feature of components is that they have their own complete life cycle , So we can React Components are treated as small, self running systems , It has its own internal state 、 Input and output .
Input
about React In terms of components , The source of its input is Props, We will use the following method to React Components pass in data :
// Title.jsx class Title extends React.Component { render() { return <h1>{ this.props.text }</h1>; } }; Title.propTypes = { text: React.PropTypes.string }; Title.defaultProps = { text: 'Hello world' }; // App.jsx class App extends React.Component { render() { return <Title text='Hello React' />; } };
text
yes Text
Component's own input field , Parent component App
Using subcomponents Title
The time should be to provide text
Property value . In addition to the standard attribute names , We will also use the following two settings :
- propTypes: Used for definition Props The type of , This helps to keep track of what's wrong at runtime Prop value .
- defaultProps: Definition Props The default value of , This is very helpful when developing
Props There is also a special attribute in the props.children
It allows us to use subcomponents :
class Title extends React.Component { render() { return ( <h1> { this.props.text } { this.props.children } </h1> ); } }; class App extends React.Component { render() { return ( <Title text='Hello React'> <span>community</span> </Title> ); } };
Be careful , If we don't take the initiative in Title
Component's render
Function {this.props.children}
, that span
Tags are not rendered . except Props outside , Another implicit component input is context
, Whole React The component tree will have a context
object , It can be accessed by every component mounted in the tree , For more information on this section, please refer to Dependency injection This chapter .
Output
The most obvious output of the component is rendered HTML Text , That is React Visual display of component rendering results . Of course , Some components that contain logic may also send or trigger some Action perhaps Event.
class Title extends React.Component { render() { return ( <h1> <a onClick={ this.props.logoClicked }> <img src='path/to/logo.png' /> </a> </h1> ); } }; class App extends React.Component { render() { return <Title logoClicked={ this.logoClicked } />; } logoClicked() { console.log('logo clicked'); } };
stay App
In the component, we want to Title
Components are passed in from Title
Called callback function , stay logoClicked
Function, we can set or modify the data that needs to be returned to the parent component . It should be noted that ,React There is no way to access the state of the subcomponent API, In other words , We can't use this.props.children[0].state
Or something like that . The correct way to get data from a sub component should be in Props The callback function is passed in , And this isolation also helps us to define... More clearly API And it promotes the so-called one-way data flow .
Composition
React One of the biggest features is the composability of its powerful components , In fact, except React outside , I don't know which framework can provide such an easy-to-use way to create and combine various components . In this chapter, we will discuss some common combination techniques , Let's take a simple example to explain . Suppose there is a header column in our application , And it has a navigation bar . We created three separate React Components :App
,Header
as well as Navigation
. Nest and combine these three components in turn , You can get the following code :
<App> <Header> <Navigation> ... </Navigation> </Header> </App>
And in the JSX The way to combine these components in is to refer to them when needed :
// app.jsx import Header from './Header.jsx'; export default class App extends React.Component { render() { return <Header />; } } // Header.jsx import Navigation from './Navigation.jsx'; export default class Header extends React.Component { render() { return <header><Navigation /></header>; } } // Navigation.jsx export default class Navigation extends React.Component { render() { return (<nav> ... </nav>); } }
However, this method may have the following problems :
- We will
App
As a connecting line between components , It's also the entry point for the entire application , So inApp
It's a good way to combine individual components in . howeverHeader
Elements may contain icons like 、 Search bar or Slogan Such an element . And if we need another one that doesn't containNavigation
FunctionalHeader
When the component , Like the one above will directlyNavigation
Component hard coded intoHeader
It will be difficult to modify the way . - This hard coded approach can be difficult to test , If we were
Header
Add some custom business logic code , So when we test, when we want to createHeader
When an instance , Because of its dependence on other components, this dependency level is too deep ( It doesn't include Shallow Rendering In this way, only the parent component is rendered, and the nested child component is not rendered ).
Use React Of children
API
React For us this.props.children
To allow a parent component to access its child components , This way helps to ensure that our Header
Independent and does not need to be decoupled from other components .
// App.jsx export default class App extends React.Component { render() { return ( <Header> <Navigation /> </Header> ); } } // Header.jsx export default class Header extends React.Component { render() { return <header>{ this.props.children }</header>; } };
It also helps to test , We can choose to enter blank div
Elements , So we can isolate the target elements to be tested and focus on the parts we need to test .
Pass the child component as an attribute
React Components can accept Props As input , We can also choose the components that will need to be encapsulated to Props Mode in :
// App.jsx class App extends React.Component { render() { var title = <h1>Hello there!</h1>; return ( <Header title={ title }> <Navigation /> </Header> ); } }; // Header.jsx export default class Header extends React.Component { render() { return ( <header> { this.props.title } <hr /> { this.props.children } </header> ); } };
This approach works well when we need to make some modifications to the incoming components to be composed .
Higher-order components
Higher-Order Components Patterns look very similar to decorator patterns , It will be used to wrap a component and add some new functionality to it . Here's a simple one for constructing Higher-Order Component Function of :
var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} /> ) } }; export default enhanceComponent;
Usually we build a factory function , Take the original component and return a so-called enhanced or wrapped version , for example :
var OriginalComponent = () => <p>Hello world.</p>; class App extends React.Component { render() { return React.createElement(enhanceComponent(OriginalComponent)); } };
Generally speaking , The first job of a high-level component is to render the original component , We often will also Props And State Pass in , Passing these two attributes in will help us set up a data broker .HOC Patterns allow us to control the input of a component , The incoming data will be needed to Props Pass in . For example, we need to add some configuration to the original component :
var config = require('path/to/configuration'); var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ config.appTitle } /> ) } };
Here for configuration
The details of the high-level components will be hidden , The original component only needs to know from Props Get to the title
Variables are then rendered to the interface . The original component doesn't care where the variables are located , To come from , The biggest advantage of this model is that we can test the component in a separate mode , And can be very convenient for the component Mocking. stay HOC In mode, our original component will look like this :
var OriginalComponent = (props) => <p>{ props.title }</p>;
Dependency injection
Most of the components and modules we write contain dependencies , Proper dependency management helps to create a well maintainable project structure . The so-called dependency injection technology is a common skill to solve this problem , Whether in the Java Or in other applications , Dependency injection is widely used . and React The need for dependency injection in is also obvious , Let's assume the following application tree structure :
// Title.jsx export default function Title(props) { return <h1>{ props.title }</h1>; } // Header.jsx import Title from './Title.jsx'; export default function Header() { return ( <header> <Title /> </header> ); } // App.jsx import Header from './Header.jsx'; class App extends React.Component { constructor(props) { super(props); this.state = { title: 'React in patterns' }; } render() { return <Header />; } };
title
The value of this variable is in App
Defined in the component , We need to pass it on to Title
In the component . The most direct way is to take it from App
The component is passed in to Header
Components , And then by Header
The component is passed in to Title
In the component . This method is very clear and maintainable in the simple three component application described here , However, with the increase of project function and complexity , This hierarchical way of passing values will cause many components to consider properties they don't need . As mentioned above HOC We have used data injection in the pattern , Here we use the same technology to inject title
Variable :
// enhance.jsx var title = 'React in patterns'; var enhanceComponent = (Component) => class Enhance extends React.Component { render() { return ( <Component {...this.state} {...this.props} title={ title } /> ) } }; export default enhanceComponent; // Header.jsx import enhance from './enhance.jsx'; import Title from './Title.jsx'; var EnhancedTitle = enhance(Title); export default function Header() { return ( <header> <EnhancedTitle /> </header> ); }
In this case HOC In the pattern ,title
Variables are contained in a hidden middle layer , We regard it as Props Values are passed in to the original Title
Variable and get a new component . This way of thinking is good , But only part of the problem has been solved . Now we can not explicitly put title
Variables are passed to Title
The same can be achieved in components enhance.jsx
effect .
React For us context
The concept of ,context
It runs through the whole thing React The object that the component tree allows each component to access . It's kind of like the so-called Event Bus, A simple example is shown below :
// a place where we'll define the context var context = { title: 'React in patterns' }; class App extends React.Component { getChildContext() { return context; } ... }; App.childContextTypes = { title: React.PropTypes.string }; // a place where we need data class Inject extends React.Component { render() { var title = this.context.title; ... } } Inject.contextTypes = { title: React.PropTypes.string };
Be careful , We're going to use context The object must pass through childContextTypes
And contextTypes
Indicate its composition . If in context
This is not specified in the object, so context
Will be set to empty , This may add some extra code . So we'd better not put context
As a simple object Object and set some encapsulation methods for it :
// dependencies.js export default { data: {}, get(key) { return this.data[key]; }, register(key, value) { this.data[key] = value; } }
such , our App
The component will be transformed into this :
import dependencies from './dependencies'; dependencies.register('title', 'React in patterns'); class App extends React.Component { getChildContext() { return dependencies; } render() { return <Header />; } }; App.childContextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func };
And in the Title
In the component , We need to make the following settings :
// Title.jsx export default class Title extends React.Component { render() { return <h1>{ this.context.get('title') }</h1> } } Title.contextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func };
Of course, we don't want to use it every time contextTypes
You need to explicitly state , We can include these declaration details in a high-level component .
// Title.jsx import wire from './wire'; function Title(props) { return <h1>{ props.title }</h1>; } export default wire(Title, ['title'], function resolve(title) { return { title }; });
there wire
The first argument to the function is React Component object , The second parameter is a set of dependency values that need to be injected , Be careful , These dependency values must have called register
function . The last parameter is the so-called mapping function , It is received and stored in context
And then returns React Props The required value in . Because in this case context
The value stored in the and Title
The required values in the component are title
Variable , So we can just go back . But in a real application, it could be a data set 、 The configuration, etc. .
export default function wire(Component, dependencies, mapper) { class Inject extends React.Component { render() { var resolved = dependencies.map(this.context.get.bind(this.context)); var props = mapper(...resolved); return React.createElement(Component, props); } } Inject.contextTypes = { data: React.PropTypes.object, get: React.PropTypes.func, register: React.PropTypes.func }; return Inject; };
there Inject It's someone who can access context
High level components of , and mapper
It's used to receive context
And convert it to what the components need Props Function of . In fact, most of today's dependency injection solutions are based on context
, I think it makes sense to understand the underlying principles of this approach . For example, now popular Redux
, At its core connect
Function and Provider
Components are based on context
.
One direction data flow
One way data flow is React The main data-driven model in , The core idea is that components do not modify the data they receive , They're just responsible for receiving new data and then rendering it back to the interface or sending out something Action To trigger some special business code to modify the data in the data store . Let's set up a button containing Switcher
Components , When we click the button, it triggers a certain flag
Changes in variables :
class Switcher extends React.Component { constructor(props) { super(props); this.state = { flag: false }; this._onButtonClick = e => this.setState({ flag: !this.state.flag }); } render() { return ( <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; // ... and we render it class App extends React.Component { render() { return <Switcher />; } };
At this point, we put all the data into the component , In other words ,Switcher
It's the only one that includes us flag
Where the variables are , Let's try hosting this data in a dedicated Store in :
var Store = { _flag: false, set: function(value) { this._flag = value; }, get: function() { return this._flag; } }; class Switcher extends React.Component { constructor(props) { super(props); this.state = { flag: false }; this._onButtonClick = e => { this.setState({ flag: !this.state.flag }, () => { this.props.onChange(this.state.flag); }); } } render() { return ( <button onClick={ this._onButtonClick }> { this.state.flag ? 'lights on' : 'lights off' } </button> ); } }; class App extends React.Component { render() { return <Switcher onChange={ Store.set.bind(Store) } />; } };
there Store
An object is a simple singleton object , It can help us set and get _flag
Property value . And through the getter
Function is passed into the component , We can be allowed to Store
External modification of these variables , At this time, our application workflow is like this :
User's input | Switcher -------> Store
Suppose we've already put flag
Values are saved to a back-end service , We need to set an appropriate initial state for the component . The problem is that the same data is stored in two places , about UI And Store
Each has its own separate account of flag
Data status of , We are equal to Store
And Switcher
Between the establishment of a two-way data flow :Store ---> Switcher
And Switcher ---> Store
// ... in App component <Switcher value={ Store.get() } onChange={ Store.set.bind(Store) } /> // ... in Switcher component constructor(props) { super(props); this.state = { flag: this.props.value }; ...
At this point, our data flow becomes :
User's input | Switcher <-------> Store ^ | | | | | | v Service communicating with our backend
In this two-way data flow , If we change on the outside Store
After the state in , We need to update the latest value after the change to Switcher
in , This also increases the complexity of the application . One way data flow solves this problem , It forces only one state store to be kept globally , It is usually stored in Store in . Under one-way data flow , We need to add some subscriptions Store The response function of state change in :
var Store = { _handlers: [], _flag: '', onChange: function(handler) { this._handlers.push(handler); }, set: function(value) { this._flag = value; this._handlers.forEach(handler => handler()) }, get: function() { return this._flag; } };
And then we were in App
The hook function is set in the component , So every time Store
When we change the value, we will force re rendering :
class App extends React.Component { constructor(props) { super(props); Store.onChange(this.forceUpdate.bind(this)); } render() { return ( <div> <Switcher value={ Store.get() } onChange={ Store.set.bind(Store) } /> </div> ); } };
Be careful , the forceUpdate
It's not a recommended usage , We usually use them HOC Mode to re render , Use here forceUpdate
It's just for demonstration . Based on the above transformation , We don't need to keep the internal state in the component :
class Switcher extends React.Component { constructor(props) { super(props); this._onButtonClick = e => { this.props.onChange(!this.props.value); } } render() { return ( <button onClick={ this._onButtonClick }> { this.props.value ? 'lights on' : 'lights off' } </button> ); } };
The advantage of this pattern is that it will transform our components into simple Store
The presentation of data in , This is the real stateless View. We can write components in a completely declarative way , And put the complex business logic in the application in a separate place . At this point, the flow graph of our application becomes :
Service communicating with our backend ^ | v Store <----- | | v | Switcher ----> ^ | | User input
In this one-way data flow, we no longer need to synchronize multiple parts of the system , This concept of one-way data flow is not only applicable to the application based on React Application .
Flux
About Flux Can refer to the author's simple understanding of GUI Ten years of evolution of application architecture :MVC,MVP,MVVM,Unidirectional,Clean
Flux Is an architectural pattern for building user interface , The earliest by Facebook stay f8 It was proposed at the meeting that , Since then , A lot of companies are trying this concept, and it seems like a great way to build front-end applications .Flux Often with React Use together , In my daily work, I also use React+Flux The collocation of , It brings me a lot of traversal .
Flux The most important role in is Dispatcher, It's all in the system Events The transfer station of .Dispatcher Responsible for receiving what we call Actions And forward it to all Stores. Every Store The instance itself decides whether or not to Action Be interested in and change its internal state accordingly . When we will Flux And familiar MVC Comparison , You'll find out Store It is similar in some sense to Model, Both are used to store States and changes in states . And in the system , except View Layer user interaction may trigger Actions outside , Others are similar to Service Layers can also trigger Actions, For example, in some HTTP After the request is completed , The request module will also issue the corresponding type of Action To trigger Store Changes to state in .
And in the Flux One of the biggest pitfalls is the destruction of data streams , We can do it in Views Medium visit Store Data in , But we shouldn't be in Views Modify any Store Internal state of , All changes to the state should be made through Actions Conduct . The author here introduces one of its maintenance Flux A variant of the project fluxiny.
Dispatcher
Most of the time, we only need a single Dispatcher, It's a sort of adhesive that combines the rest of the system together .Dispatcher Generally speaking, there are two inputs :Actions And Stores. among Actions Need to be forwarded directly to Stores, So we don't need to record Actions The object of , and Stores A reference to a must be saved in Dispatcher in . Based on this consideration , We can write a simple Dispatcher:
var Dispatcher = function () { return { _stores: [], register: function (store) { this._stores.push({ store: store }); }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action); }); } } } };
In the above implementation, we will find that , Every incoming Store
Every object should have a update
Method , So we're doing Store To check whether the method exists :
register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { this._stores.push({ store: store }); } }
After finishing, for Store After registration of , The next step is that we need to put View And Store Connect , Thus in Store It can trigger when a change occurs View Re rendering of :
quite a lot flux The following auxiliary functions will be used in the implementation of :
Framework.attachToStore(view, store);
But the author doesn't really like it , This will require View You need to call a specific API, In other words , stay View You need to know Store Implementation details , And make View And Store It's in a tight coupling situation again . When developers plan to switch to other Flux You have to modify every frame View It's the same as API, That would add to the complexity of the project . Another alternative is to use React mixins
:
var View = React.createClass({ mixins: [Framework.attachToStore(store)] ... });
Use mixin
It's a good idea to modify the existing React Component without affecting its original code , But the drawback of this approach is that it can't be done in a way that Predictable To modify components in a way that , Users are less controllable . Another way is to use React context
, This approach allows us to pass values across levels to React Components in the component tree without knowing which level they are in the component tree . This way and mixins There may be the same problem , Developers don't know where the data comes from .
The author finally chooses the way mentioned above Higher-Order Components Pattern , It creates a wrapper function to repackage existing components :
function attachToStore(Component, store, consumer) { const Wrapper = React.createClass({ getInitialState() { return consumer(this.props, store); }, componentDidMount() { store.onChangeEvent(this._handleStoreChange); }, componentWillUnmount() { store.offChangeEvent(this._handleStoreChange); }, _handleStoreChange() { if (this.isMounted()) { this.setState(consumer(this.props, store)); } }, render() { return <Component {...this.props} {...this.state} />; } }); return Wrapper; };
among Component
We need to attach to Store
Medium View, and consumer
It should be passed on to View Of Store The state of the parts in , The simple usage is :
class MyView extends React.Component { ... } ProfilePage = connectToStores(MyView, store, (props, store) => ({ data: store.get('key') }));
The advantage of this model is that it effectively divides the responsibilities between modules , In this mode Store There is no need to actively push messages to View, The master needs to simply modify the data and broadcast that my status has been updated , Then from HOC To actively grab data . Then in the author's concrete implementation , That is to say HOC Pattern :
register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer) { consumers.push(consumer); }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } }
Another common user scenario is that we need to provide some default state for the interface , In other words, when everyone consumer
Some data is required for initialization :
var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; };
in summary , The final Dispatcher The function is shown below :
var Dispatcher = function () { return { _stores: [], register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } } } };
Actions
Actions It is the message carrier that is passed between each module in the system , The author thinks that we should use standard Flux Action Pattern :
{ type: 'USER_LOGIN_REQUEST', payload: { username: '...', password: '...' } }
Among them type
The property indicates that Action It represents the operation of payload
It contains relevant data . in addition , In some cases Action There is no Payload, So you can use Partial Application Way to create standard Action request :
var createAction = function (type) { if (!type) { throw new Error('Please, provide action\'s type.'); } else { return function (payload) { return dispatcher.dispatch({ type: type, payload: payload }); } } }
Final Code
We've seen the core of Dispatcher And Action Construction process of , So here we combine the two :
var createSubscriber = function (store) { return dispatcher.register(store); }
And in order not to expose directly dispatcher object , We can allow users to use createAction
And createSubscriber
These two functions :
var Dispatcher = function () { return { _stores: [], register: function (store) { if (!store || !store.update) { throw new Error('You should provide a store that has an `update` method.'); } else { var consumers = []; var change = function () { consumers.forEach(function (l) { l(store); }); }; var subscribe = function (consumer, noInit) { consumers.push(consumer); !noInit ? consumer(store) : null; }; this._stores.push({ store: store, change: change }); return subscribe; } return false; }, dispatch: function (action) { if (this._stores.length > 0) { this._stores.forEach(function (entry) { entry.store.update(action, entry.change); }); } } } }; module.exports = { create: function () { var dispatcher = Dispatcher(); return { createAction: function (type) { if (!type) { throw new Error('Please, provide action\'s type.'); } else { return function (payload) { return dispatcher.dispatch({ type: type, payload: payload }); } } }, createSubscriber: function (store) { return dispatcher.register(store); } } } };
Participation of this paper Tencent cloud media sharing plan , You are welcome to join us , share .
版权声明
本文为[:::::::]所创,转载请带上原文链接,感谢
边栏推荐
- 一篇文章带你了解HTML表格及其主要属性介绍
- Analysis of react high order components
- Let the front-end siege division develop independently from the back-end: Mock.js
- Network security engineer Demo: the original * * is to get your computer administrator rights! 【***】
- Python + appium automatic operation wechat is enough
- How long does it take you to work out an object-oriented programming interview question from Ali school?
- I'm afraid that the spread sequence calculation of arbitrage strategy is not as simple as you think
- Filecoin的经济模型与未来价值是如何支撑FIL币价格破千的
- Python3 e-learning case 4: writing web proxy
- In order to save money, I learned PHP in one day!
猜你喜欢
一篇文章带你了解CSS3 背景知识
How to encapsulate distributed locks more elegantly
一篇文章带你了解SVG 渐变知识
人工智能学什么课程?它将替代人类工作?
Linked blocking Queue Analysis of blocking queue
Flink的DataSource三部曲之二:内置connector
[JMeter] two ways to realize interface Association: regular representation extractor and JSON extractor
It's so embarrassing, fans broke ten thousand, used for a year!
How do the general bottom buried points do?
Not long after graduation, he earned 20000 yuan from private work!
随机推荐
至联云解析:IPFS/Filecoin挖矿为什么这么难?
大数据应用的重要性体现在方方面面
The practice of the architecture of Internet public opinion system
Summary of common algorithms of linked list
如何将数据变成资产?吸引数据科学家
The difference between Es5 class and ES6 class
使用 Iceberg on Kubernetes 打造新一代云原生数据湖
Network security engineer Demo: the original * * is to get your computer administrator rights! 【***】
6.3 handlerexceptionresolver exception handling (in-depth analysis of SSM and project practice)
小程序入门到精通(二):了解小程序开发4个重要文件
Mongodb (from 0 to 1), 11 days mongodb primary to intermediate advanced secret
Use of vuepress
PHPSHE 短信插件说明
Brief introduction and advantages and disadvantages of deepwalk model
從小公司進入大廠,我都做對了哪些事?
ES6学习笔记(四):教你轻松搞懂ES6的新增语法
vue任意关系组件通信与跨组件监听状态 vue-communication
Why do private enterprises do party building? ——Special subject study of geek state holding Party branch
Python自动化测试学习哪些知识?
6.6.1 localeresolver internationalization parser (1) (in-depth analysis of SSM and project practice)