React 设计模式

本文介绍了React组件设计的几种模式,包括容器与展示组件、高阶组件、render props、context模式、组合组件以及继承模式。重点讲解了这些模式的原理、应用场景及优缺点,旨在帮助开发者更好地组织和复用React组件代码。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

(一)前言

这里提到的设计模式并不是编程通用的设计模式,如常说的单例模式、抽象工厂模式、建造者模式、工厂模式、原型模式等。而是在设计 React 组件时的一些解决方案与技巧,包括以下几种:
(1) 容器与展示组件
(2) 高阶组件
(3) render props
(4) context 模式
(5) 组合组件
(6) 继承

当然概念部分,大家能根据名字猜出,但是我还是要为每种,单独给出demo详细说下。
设计模式本身是因为产品需求,解决特定的物业场景,并非完全需要按照这样,所以个人开发中建议形成自己的开发模式。

(二)容器与展示组件

这可能是目前应用最广泛,也是最简单的,大家最推崇的设计方案。其实大概意思就是,将组件分为两种。
一个为容器组件,负责与外部数据进行交互,比如处理业务逻辑(连接redux),
一个为展示组件,只通过props传递业务逻辑,state控制内部交互,不包含任何业务逻辑,也不与外部数据源(连接redux)进行沟通,现在我来举个例子。

需求: 当我们点击按钮后,会去接口拉去数据,并将返回的数据渲染到视图,当然这种需求,简单可以将全部视图写在一起,但是我们需要使用容器和展示组件的方式来组织代码,那么代码如下

// 容器组件 textContainer.js
import React from 'react';
import Text from './text';

class TextContainer extends React.Component {
  state = {
    text: '',
  };

  getData = () => {
    // 模拟异步请求
    setTimeout(() => {
      this.setState({ text: '测试数据' });
    }, 1000);
  };

  render() {
    const {
      state: {
        text,
      },
    } = this;
    return (
      <div>
        <Text text={text} onClick={this.getData} />
      </div>
    );
  }
}

export default TextContainer;

// 展示组件 text.js
import React from 'react';
import PropTypes from 'prop-types';

class Text extends React.PureComponent {
  render() {
    const {
      props: {
        text,
        onClick,
      },
    } = this;
    return (
      <div>
        <div>
        接口返回的数据:
          {text}
        </div>
        <button onClick={onClick}>点我啊</button>
      </div>
    );
  }
}

// 这里我建议,不是必须传入的参数,尽量不使用isRequired验证
// 比如input value 肯定是必须的 但是如果不做受控组件,那么回调onChange就不是必须
Text.defaultProps = {
  text: '',
  onClick: () => null,
};

Text.propTypes = {
  text: PropTypes.string,
  onClick: PropTypes.func,
};

export default Text;

从上面代码我们看出,我们采用软件设计原则中的“责任分离”, 即让一个模块只负责责任尽量单一

容器展示组件这个模式所解决的问题在于,当我们切换数据获取方式时,只需在容器组件修改相应逻辑即可,展示组件无需做改动。展示组件可完全不变,展示组件有了更高的可复用性。

但该模式的缺点也在于将一个组件分成了两部分,增加了代码跳转的成本。并不是说组件包含从外部获取数据,就要将其拆成容器组件与展示组件。

(三)高阶组件

1.概念:
高阶组件不是组件,是 增强函数,可以输入一个元组件,返回出一个新的增强组件;
高阶组件的主要作用是 代码复用,操作状态和参数;

2. 分类(主要两个。一个属性代理,一个反向继承)

  1. 属性代理 : 返回出一个组件,它基于被包裹组件进行功能增强

这里有个获取display name的函数,下面新增的高阶组件都会调用这个方法,就不再次书写了

// 获取display name
function getDisplayName(WrappedComponent) {
  return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

1.1 默认参数: 可以为组件包裹一层默认参数;

export function ProxyHoc(Component) {
  const NewComponent = (props) => {
    const newProps = {
      name: '高阶组件增加的属性',
      age: 1,
    };
    return <Component {...props} {...newProps} />;
  };
  return NewComponent;
}

1.2 提取状态: 可以通过 props 将被包裹组件中的 state 依赖外层,例如用于转换受控组件:
现在我们来实现一个简单表单高阶组件,不包含表单验证。

// withForm.js
import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
import { getDisplayName } from './utils';

export default function WithForm(Component) {
  class Enhance extends React.Component {
    static displayName = `WithForm(${getDisplayName(Component)})`;

    state = {
      form: {},
    };

    onChange = key => (e) => {
      const { form } = this.state;
      form[key] = e.target.value;
      this.setState({
        form,
      });
    };

    handleSubmit = () => this.state.form;

    getField = fieldName => ({
      onChange: this.onChange(fieldName),
    });

    render() {
      const newProps = {
        ...this.props,
        getForm: this.handleSubmit,
        setFormItem: this.getField,
      };

      return (<Component {...newProps} />);
    }
  }
  // 自动拷贝所有非 React 的静态方法
  hoistNonReactStatic(Enhance, Component);
  return Enhance;
}

我们需要使用这个表单控件时候,那么代码如下

import React from 'react';

// components
import HocForm from './components/hoc/hocForm';

// style
import './app.css';

class App extends React.Component {
  render() {
    const {
      props: { setFormItem, getForm },
    } = this;
    return (
      <div className="App">
        <header className="App-header">
          <input
            type="text"
            {...setFormItem('username')}
            placeholder="用户名"
          />
          <input
            type="password"
            placeholder="密码"
            {...setFormItem('password')}
          />
          <div>
            表单数据:
            {JSON.stringify(getForm())}
          </div>
        </header>
      </div>
    );
  }
}

export default HocForm(App);

我们可以通过props,设置表单需要的key,最后调用getForm得到内部表单数据。

1.3 包裹组件: 可以为被包裹元素进行一层包装,

import React from 'react';
import hoistNonReactStatic from 'hoist-non-react-statics';
import { getDisplayName } from './utils';

export default function WithMask(Component) {
  class Enhance extends React.Component {
    static displayName = `WithMask(${getDisplayName(Component)})`;

    render() {
      return (
        <div>
          <Component {...this.props} />
          <div
            style={{
              position: 'absolute',
              left: 0,
              top: 0,
              zIndex: -1,
              width: '100%',
              height: '100%',
              backgroundColor: 'rgba(0, 0, 0, 0.6)',
            }}
          />
        </div>
      );
    }
  }
  // 自动拷贝所有非 React 的静态方法
  hoistNonReactStatic(Enhance, Component);
  return Enhance;
}

这里给mask封装来一层背景包裹组件。

  1. 反向继承 : 返回出一个组件,继承于被包裹组件,常用于以下操作:
export default function WithHocSimple(Component) {
  return class extends Component {
    render() {
      return super.render();
    }
  };
}

反向继承其实就是 一个函数接受一个 Component 组件作为参数传入,并返回一个继承了该传入 Component 组件的类,且在该类的 render() 方法中返回 super.render() 方法。

会发现其属性代理和反向继承的实现有些类似的地方,都是返回一个继承了某个父类的子类,只不过属性代理中继承的是 React.Component,反向继承中继承的是传入的组件 Component

反向继承可以用来做什么:

操作 state
渲染劫持(Render Highjacking)

2.1 操作state

高阶组件中可以读取、编辑和删除 WrappedComponent 组件实例中的 state。甚至可以增加更多的 state 项,但是 非常不建议这么做 因为这可能会导致 state 难以维护及管理。

import React from 'react';

export default function WithHocLoading(Component) {
  return class extends Component {
    render() {
      return (
        <div>
          <h2>Debugger Component Logging...</h2>
          <p>state:</p>
          <pre>{JSON.stringify(this.state, null, 4)}</pre>
          <p>props:</p>
          <pre>{JSON.stringify(this.props, null, 4)}</pre>
          {super.render()}
        </div>
      );
    }
  };
}

在这个例子中利用高阶函数中可以读取 state 和 props 的特性,对 Component 组件做了额外元素的嵌套,把 Component 组件的 state 和 props 都打印了出来,

2.2 渲染劫持

我们来实现一个通过修改由 render() 输出的 React 元素树

import React from 'react';

export default function WithTree(Component) {
  return class extends Component {
    render() {
      const tree = super.render();
      const newProps = {};
      if (tree && tree.type === 'div') {
        newProps.className = 'App add-view';
      }
      const props = {
        ...tree.props,
        ...newProps,
      };
      const newTree = React.cloneElement(tree, props, tree.props.children);
      return newTree;
    }
  };
}

模式所解决的问题
同样的逻辑我们总不能重复写多次。高阶组件起到了抽离共通逻辑的作用。同时高阶组件的嵌套使用使得代码复用更加灵活了。

注意以下几点

  1. 包装显示名称以便轻松调试(定义displayName)
  2. 不要在 render 方法中使用 HOC(会导致每次都重新渲染整个HOC,而且之前的状态会丢失)
  3. 务必复制静态方法(hoist-non-react-statics库解决)
  4. Refs 不会被传递(使用React.forwardRef解决)

(四)render props
概念:
指一种在 React 组件之间使用一个值为函数的prop来共享代码的简单技术。同高阶组件一样,render props的引入也是为了解决复用业务逻辑。

需求:
我们实现一个判断性别的组件,提供给购物车和商品列表组件使用,将共用性别判断逻辑抽离出来。

// providerGender.js
import React from 'react';
import PropTypes from 'prop-types';

const ProviderGender = (props) => {
  // 判断是否是女性用户
  const isWoman = Math.random() > 0.1;
  if (isWoman) {
    const allProps = { add: '高阶组件增加的属性', ...props };
    return props.children(allProps);
  }
  return <div>女士专用,男士无权浏览</div>;
};

ProviderGender.defaultProps = {
	children: () => null,
};

ProviderGender.propTypes = {
  children: PropTypes.func,
};

export default ProviderGender;

然后我们分别定义商品列表组件

// list.js
import React from 'react';
import PropTypes from 'prop-types';

const List = ({ add, name }) => (
  <div>
    <span>{`${name}:`}</span>
    <span>{add}</span>
  </div>
);

List.defaultProps = {
  name: '',
  add: '',
};

List.propTypes = {
  name: PropTypes.string,
  add: PropTypes.string,
};

export default List;

现在我们来该如何使用两者组合

import React from 'react';

// components
// render props
import ProviderGender from './components/renderProps/providerGender';
import List from './demo/shoppingPropsDemo';

// style
import './app.css';

class App extends React.Component {
  state = {
    name: '商品名称列表',
  };

  onToggleList = () => {
    this.setState(state => ({
      ...state,
      name: state.name === '商品名称列表' ? '购物车列表' : '商品名称列表',
    }));
  };

  render() {
    const {
      state: { name },
    } = this;
    return (
      <div className="App">
        <header className="App-header">
          <ProviderGender>
            {props => <List add={props.add} name={name} />}
          </ProviderGender>
          <button
            type="button"
            onClick={this.onToggleList}
          >
            点我
          </button>
        </header>
      </div>
    );
  }
}

export default App;

上面代码中可以看出,属性代理的方案高阶组件也能实现,但是 render props 却有更加强大的一个点就是,因为可以在render中,所以能自定义给list组件,添加新的props,而不需要修改ProviderGender组件,这是高阶不能在render中调用,所不具备的最大优势。

当然,render props 的使用可以不局限在利用 children,组件任意的 prop 属性都可以达到相同效果,比如我们用 renderChildren 这个 prop 实现上面相同的效果。

比如我们将children改为renderChildren函数。

// providerGender.js
import React from 'react';
import PropTypes from 'prop-types';

const ProviderGender = (props) => {
  // 判断是否是女性用户
  const isWoman = Math.random() > 0.1;
  if (isWoman) {
    const allProps = { add: '高阶组件增加的属性', ...props };
    return props.renderChildren(allProps);
  }
  return <div>女士专用,男士无权浏览</div>;
};

ProviderGender.defaultProps = {
  renderChildren: () => null,
};

ProviderGender.propTypes = {
  renderChildren: PropTypes.func,
};

export default ProviderGender;

// app.js
class App extends React.Component {
	render() {
	    const {
	      state: { name },
	    } = this;
	    renturn (
	    <div>
	    	<ProviderGender
			   renderChildren={props => <List add={props.add} name={name} />}
			/>
	    </div>
	    )
	}
}

注意事项:

将 Render Props 与 React.PureComponent 一起使用时要小心!如果你在 Provider 属性中创建函数,那么使用 render props 会抵消使用React.PureComponent 带来的优势。因为浅比较 props 的时候总会得到 false,并且在这种情况下每一个 render 对于 render props 将会生成一个新的值。

比如上面例子就是在每次render,重新生成一个新的函数作为 的 prop。那么解决方案就是,将匿名函数,提到外层组件的方法

class App extends React.Component {
  state = {
    name: '商品名称列表',
  };

  onToggleList = () => {
    this.setState(state => ({
      ...state,
      name: state.name === '商品名称列表' ? '购物车列表' : '商品名称列表',
    }));
  };

  renderList = (props) => {
    const {
      state: { name },
    } = this;
    return (
      <List add={props.add} name={name} />
    );
  };

  render() {
    return (
      <div className="App">
        <header className="App-header">
          <ProviderGender>
            {this.renderList}
          </ProviderGender>
          <button
            type="button"
            onClick={this.onToggleList}
          >
            点我
          </button>
        </header>
      </div>
    );
  }
}

(五)Context模式
概念:
React 的 Context 接口提供了一个无需为每层组件手动添加 props ,就能在组件树间进行数据传递的方法。

// themeProvider.js
import React from 'react';
import PropTypes from 'prop-types';

const ThemeContext = React.createContext();

const ThemeProvider = ThemeContext.Provider;

export const ThemeConsumer = ThemeContext.Consumer;

const Context = ({ value, children }) => (
  <div>
    <ThemeProvider value={value}>
      {children}
    </ThemeProvider>
  </div>
);

Context.defaultProps = {
  value: {},
};

Context.propTypes = {
  value: PropTypes.objectOf(PropTypes.any),
  children: PropTypes.node.isRequired,
};

export default Context;

// consumer.js
import React from 'react';
// import PropTypes from 'prop-types';

import { ThemeConsumer } from '../components/context/themeProvider';

const Consumer = () => (
  <ThemeConsumer>
    {
      theme => (
        <p
          style={{
            color: theme.textColor,
            backgroundColor: theme.mainColor,
          }}
        >
          内容区域
        </p>
      )
    }
  </ThemeConsumer>
);


Consumer.defaultProps = {};

Consumer.propTypes = {};

export default Consumer;

在app.js中调用为

import React from 'react';

// context
import ThemeProvider from './components/context/themeProvider';
import Consumer from './demo/consumer';

// style
import './app.css';

class App extends React.Component {
  render() {
    return (
      <ThemeProvider className="App" value={{ mainColor: 'blue', textColor: 'red' }}>
        <header className="App-header">
          <Consumer />
        </header>
      </ThemeProvider>
    );
  }
}

export default App;

我们看到,consumer.js中并直接传递props,通过ThemeContext.Consumer直接读取到context中的theme。

优势:
Context 主要应用场景在于很多不同层级的组件需要访问同样一些的数据,可以跨组件访问一样的数据。

(五)组合组件模式

借用组合组件,使用者只需要传递子组件,子组件所需要的props在父组件会封装好,引用子组件的时候就没必要传递所有props了。

组合组件核心的两个方法是React.Children.mapReact.cloneElement。React.Children.map 用来遍历获得组件的子元素。React.cloneElement 则用来复制元素,这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props ,我们就是利用这个函数,把想要的 props 传入子元素。

现在我们来实现一个antd的tabs组件,我们先看他如何使用的

import { Tabs } from 'antd';

const { TabPane } = Tabs;

function callback(key) {
  console.log(key);
}

ReactDOM.render(
  <Tabs defaultActiveKey="1" onChange={callback}>
    <TabPane tab="Tab 1" key="1">
      Content of Tab Pane 1
    </TabPane>
    <TabPane tab="Tab 2" key="2">
      Content of Tab Pane 2
    </TabPane>
    <TabPane tab="Tab 3" key="3">
      Content of Tab Pane 3
    </TabPane>
  </Tabs>,
  mountNode,
);

首先我们实现tabs

import React from 'react';
import PropTypes from 'prop-types';

class Tabs extends React.Component {
  state = {
    activeIndex: 0,
  };

  onClickItem = (index) => {
    this.setState({ activeIndex: index });
    this.props.onChange(index);
  };

  renderHeaderItem = () => {
    const {
      state: { activeIndex },
      props: { children },
    } = this;
    return React.Children.map(children, (childElem, index) => {
      if (!childElem.type) return null;
      const active = activeIndex === index;
      return (
        <div
          style={{
            maxWidth: '150px',
            color: active ? 'red' : 'green',
            border: active ? '1px red solid' : '0px',
          }}
          onClick={() => this.onClickItem(index)}
        >
          {childElem.props.tab}
        </div>
      );
    });
  };

  render() {
    const {
      state: { activeIndex },
      props: { children },
    } = this;
    return (
      <div>
        <div style={{ display: 'flex' }}>
          {this.renderHeaderItem()}
        </div>
        {
          React.Children.map(children, (childElem, index) => {
            if (!(childElem.type && activeIndex === index)) return null;
            return React.cloneElement(childElem, {
              activeIndex,
              index,
            });
          })
        }
      </div>
    );
  }
}

Tabs.defaultProps = {
  onChange: () => null,
};

Tabs.propTypes = {
  children: PropTypes.node.isRequired,
  onChange: PropTypes.func,
};

export default Tabs;

然后实现tabPane.js

import React from 'react';
import PropTypes from 'prop-types';

const TabPane = ({ children }) => (
  <div
    style={{
      width: '100%',
      minHeight: 400,
      border: '1px #fff solid',
    }}
  >
    {children}
  </div>
);

TabPane.defaultProps = {
  // activeIndex: 0,
  // index: 0,
  children: '',
};

TabPane.propTypes = {
  // activeIndex: PropTypes.number,
  // index: PropTypes.number,
  children: PropTypes.node,
};

export default TabPane;

在app.js中调用为

import React from 'react';

// tabs
import Tabs from './components/tabs/tabs';
import TabPane from './components/tabs/tabPane';

// style
import './app.css';

class App extends React.Component {
  render() {
    return (
      <div>
        <header className="App-header">
          <Tabs>
            <TabPane tab="One">
              <div>内容1</div>
            </TabPane>
            <TabPane tab="Two">
              <div>内容2</div>
            </TabPane>
            <TabPane tab="Three">
              <div>内容3</div>
            </TabPane>
          </Tabs>
        </header>
      </div>
    );
  }
}

export default App;

模式所解决的问题

组合组件设计模式一般应用在一些共享组件上。如 select 和 option , Tab 和TabItem 等,通过组合组件,使用者只需要传递子组件,子组件所需要的 props 在父组件会封装好,引用子组件的时候就没必要传递所有 props 了。我们可以在共享的组件中运用这种模式,简化组件使用者的调用方式,antd 当中你就能看到许多组合组件的使用。

(六)继承模式

我们最后来谈谈OOP的继承模式。

如果组件定义为class组件,那么我们当然可以使用继承的模式来实现组件的复用。我们通过一个基类来实现一些通用的逻辑,然后再通过继承分别实现两个子类。

// base.js
import React from 'react';

class Base extends React.PureComponent {
  getHeader = () => null;

  render() {
    return (
      <div style={{ border: '1px solid red', margin: 5, width: 300 }}>
        {this.getHeader()}
        <div>这里是通用逻辑</div>
      </div>
    );
  }
}

export default Base;

// demo.js
import React from 'react';
// import PropTypes from 'prop-types';

// components
import Base from '../components/oop/base';

class Demo1 extends Base {
  getHeader = () => <span>demo1</span>;
}

class Demo2 extends Base {
  getHeader = () => <span>Demo2</span>;
}


export { Demo1, Demo2 };

从上面代码可以看出,可以将通用逻辑封装在基本类中,通过继承,实现方法,达到复用。

缺点:

  1. 父类的属性和方法,子类是无条件继承的。也就是说,不管子类愿意不愿意,都必须继承父类所有的属性和方法,这样就不够灵活了。
  2. js中class并不直接支持多继承。

这两个缺点使得继承相对于组合组件缺少了灵活性以及可扩展性。

(七)结语

通过上面例子我们总结常见的模式,但是请记住,组合优于继承!组件的复用请第一时间想到使用组合而非继承。

### React设计模式概述 React设计模式是指一系列用于提高代码可维护性和重用性的最佳实践方法。这些模式不仅有助于简化开发过程,还能增强应用程序的性能和结构清晰度[^1]。 ### 关键概念 #### 容器与展示组件模式 (Container/Presentational Components) 此模式提倡将UI分为两类角色不同的组件: - **容器组件**:负责处理业务逻辑、数据获取以及与其他系统的交互;通常不包含任何DOM标记。 - **展示组件**:专注于视图层的表现形式,只关心如何渲染接收到的数据而不涉及具体操作行为。 这种分离使得两者可以独立测试和发展,并且更容易理解和管理复杂的应用程序架构[^3]。 ```jsx // 展示组件 ExamplePresentational.jsx function ExamplePresentational({ title, content }) { return ( <div> <h1>{title}</h1> <p>{content}</p> </div> ); } export default ExamplePresentational; ``` ```jsx // 容器组件 ExampleContainer.jsx import React from 'react'; import axios from 'axios'; import ExamplePresentational from './ExamplePresentational'; class ExampleContainer extends React.Component { state = { data: null }; async componentDidMount() { const response = await axios.get('/api/data'); this.setState({ data: response.data }); } render() { if (!this.state.data) return <div>Loading...</div>; return <ExamplePresentational {...this.state.data} />; } } export default ExampleContainer; ``` #### 自定义 Hooks 和记忆化模式 自定义Hooks允许开发者封装特定于某个功能的状态管理和副作用逻辑,从而促进跨多个组件之间的复用。而记忆化技术则可以通过缓存计算结果来减少不必要的重新计算次数,进而提升应用效率。 ```javascript // useCustomHook.js import { useState, useEffect } from 'react'; function useCustomHook(initialValue) { const [value, setValue] = useState(initialValue); useEffect(() => { // 执行一些初始化或清理工作... }, []); return [value, setValue]; } export default useCustomHook; ``` ### 高阶组件(Higher Order Component, HOC) 这是一种常见的函数式编程技巧,在React中用来创建新的组件版本,其中包含了额外的功能特性或是修改后的属性配置。它接受一个现有组件作为参数并返回一个新的具有相同接口但增强了某些能力的新组件实例[^2]。 ```javascript // withLogging.js function withLogging(WrappedComponent) { return class extends React.Component { componentDidMount() { console.log('Component mounted'); } componentWillUnmount() { console.log('Component will unmount'); } render() { return <WrappedComponent {...this.props} />; } }; } ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值