当前位置:网站首页>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:04 :::::::

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 in App It's a good way to combine individual components in . however Header Elements may contain icons like 、 Search bar or Slogan Such an element . And if we need another one that doesn't contain Navigation Functional Header When the component , Like the one above will directly Navigation Component hard coded into Header 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 create Header 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 childrenAPI

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 .

版权声明
本文为[:::::::]所创,转载请带上原文链接,感谢