【问题标题】:React Higher Order Elements - how far can one go with passing props反应高阶元素 - 传递道具可以走多远
【发布时间】:2016-12-11 21:51:57
【问题描述】:

正在重构其中的大型项目

  • 有些组件看起来非常相似
  • 有大量文件(非常高级别的组件与文件粒度)

我在这里和那里思考并搜索了处理这个问题的方法,发现这篇关于高阶组件 (HOC) 的精彩文章 - 基本上是包装另一个组件的组件。

https://medium.com/@franleplant/react-higher-order-components-in-depth-cf9032ee6c3e#.8vr464t20

我现在将给你 (A) 我需要处理的八个相似组件类型中的两个示例,而不是 (B) 我将粘贴我的代码想出了将这八个文件统一为一个。最后 (C) 将粘贴该统一组件的使用示例。

我会尽量保持一致,并且命名将不受域驱动(我不能在此处发布项目详细信息),但下面不同代码摘录中的相同名称将始终指向相同的组件和数据。否则我会指出它。

(A) - 相似的组件类型

1. TabA - 简单的一个

export default class TabA extends Component {
  render() {
    return (
    <PageWrapper>
      <Grid>
        <GridItem xsSize="3">
          <SmartComponent something={this.props.something }/>
        </GridItem>
        <GridItem xsSize="9">
          <Tabs 
            permalink = { this.props.permalink } 
            history={ this.props.history } 
            activeTab={ Paths.somePath }
          />
          <TabAContent
            data={ this.props.data }
            name={ this.props.name }
            someValue={ this.props.someValue } 
          />
        </GridItem>
      </Grid>
    </PageWrapper>
  );
}

}

注意 SomeComponentA 不包含任何子元素。这里也没有任何类型的条件渲染。

2。 TabB - 更复杂的一个

同样,请注意 renderSomeData 方法有条件地渲染 SmartComponentToBeConditionallyRendered 并且 SomeComponentB 从道具中获取孩子。

export default class TabB extends Component {
  renderSomeData() {

    let someData = {
      header: "Header text",
      searchPlaceHolder: 'Search (name)',
      buttonCaption: 'button caption'
    };

    return (
    <SmartComponentToBeConditionallyRendered
      type={ 'some_type' }
      permalink={ this.props.permalink }
      data={ someData }
    />
  )
}

render() {
  let { data } = this.props;

  return (
    <div>
      <PageWrapper>
        <Grid>
          <GridItem xsSize="3">
            <SmartComponent something={this.props.something}/>
          </GridItem>
        <GridItem xsSize="9">
          <Tabs 
            permalink = { this.props.permalink } 
            history = { this.props.history } 
            activeTab = { Paths.somePage }
          />
          <TabBContent data = { data }>
            {this.props.children}
          </TabBContent>
        </GridItem>
      </Grid>
    </PageWrapper>
    {
      this.context.hasPermission('somePermission') ?
        this.renderSomeData() :
        null
    }
    </div>
  )
}
static contextTypes = {
  hasPermission: React.PropTypes.func.isRequired
}

}

我在开始时写过的这八个组成部分 - 它们都代表了三种可能性中的一种。 上图中的两个和可能性 C,但 C 中的差异只是另一个条件渲染组件,因此不值得一提,因为它最终会归结为在 props 中传递更多标志。

以上这两个组件 - 它们的不同之处在于:

  • 一种 SomeComponentX - 在这八个相似的组件中的每一个中都可能有 A、B 和 C、D、E 等来代替 X。每个 SomeComponentX 也接受不同的道具。
  • Paths.VALUE_HERE
  • SomeComponentX 是否接收任何子项
  • 如果他们有条件地从 renderSomeData 方法渲染数据
  • 如果是 - 方法内部定义的 someData 也会发生变化
  • 固定链接
  • some_type

(B) 我的想法

let availablePartials = {
  PartialA: PartialA,
  PartialB: PartialB,
  PartialC: PartialC
}

export default class GenericTab extends Component {
  renderSomeData() {
    return (
      <SomeData
        type      = { this.props.type }
        permalink = { this.props.permalink }
        data      = { this.props.someData } //PASSED FROM PROPS NOW
      />
    );
  }

  render() {
    let  tabContent = React.createElement(
      availablePartials[this.props.partialView.name], 
      this.props.partialView.props, 
      this.props.renderChildren ? this.props.children : null
    );

    return (
      <div>
        <PageWrapper>
          <Grid>
            <GridItem xsSize="3">
              <SmartComponent something = { this.props.permalink }/>
            </GridItem>
            <GridItem xsSize = "9">
              <Tabs 
                permalink = { this.props.permalink } 
                history   = { this.props.history }
                activeTab = { this.props.activeTab }
              />
               { tabContent }
            </GridItem>
          </Grid>
        </PageWrapper>
        {
          this.context.hasPermission(this.props.requiredPermission) &&     this.props.dataForSomeDataMethod ?
            this.renderSomeData() 
            : null
        }
      </div>
    )
  }
  static contextTypes = {
    hasPermission: React.PropTypes.func.isRequired
  }
};

CityPageTab.propTypes = {
  permalink: PropTypes.string,
  dataForSomeDataMethod: PropTypes.object,
  type: PropTypes.string,
  activeTab: PropTypes.string,
  renderChildren: PropTypes.bool,
  partialView: PropTypes.object,
  requiredPermission: PropTypes.string
};

基本上一切都是由道具构成的。我唯一不喜欢的部分是 availablePartials[this.props.partialView.name]。 它要求开发人员保持availablePartials 对象的状态一致,并且有点纠结。不是很好的解决方案,但到目前为止我想出的仍然是最好的。

(C) 新的 GenericTab 使用示例

componentThatUseGenericTabRenderMethod() {
  let { valueA, valueB, valueC, history } = this.props;
  let someData = {
    header: 'header text',
    searchPlaceHolder: 'Search (name)',
    buttonCaption: 'buttonCaption'
  }

  return (
    <GenericTab
      partialView = {{
        name: 'PartialA', 
        props: {
          A: valueA,
          B: valueB,
          C: valueC,
          history: history,
          permalink: this.props.params.permalink
        }
      }}
      permalink = { this.props.params.permalink } 
      activeTab = { Paths.somePath }
      someData = { someData }
      type = { 'SOME_TYPE' }
      renderChildren = { false }
      requiredPermission = { 'some_required_permision' }
    />
  );
}

原来如此。用法变得有点复杂,但我删除了七个文件(删除文件是主要目标,因为它们太多了)并且将以类似的方式进一步推动它 - 通用的。 具有通用性的东西 - 它更难使用但节省了大量空间。

Project 使用 Redux,所以不要太在意将 props 向下传递。它们总是只来自一些渲染 GenericTab 的 SmartParentComponent

下面是它在页面上的外观的可视化。 GenericTab 负责渲染 Tabs 和 TabContent 部分。 是的,我知道这是一个糟糕的解决方案,但我不负责它的架构。这里有很多东西需要重构,而我所问的只是旅程中的一步。因此,请让我们专注于所提出的问题,而不是其他与此代码有很大问题的事情。我知道。:)

我想我可以用它写一篇文章,但我真的没有博客来做这件事:)。

请告诉我您的想法、建议升级、处理此问题的不同方法等。

【问题讨论】:

  • 您能否添加一些关于选项卡外观的可视化表示?在这里理解整个事情有点困难,但我已经可以告诉你,你的组件负责渲染很多东西,你应该把它分成几个更小的组件。
  • hm,对我来说唯一有意义的拆分就是加入renderSomeData。我可以提取它,这是真的。除此之外,我在这里看不到很多可能性。项目很混乱,还有很多事情要做。是的,我会尝试编辑我的问题并添加可视化效果。
  • 我相信这有点可能,当您编辑您的问题时,我想我可以帮助您这样做
  • @Pcriulan done: ) 我还更改了代码中的命名,因此它现在类似于图像中的命名。我知道逻辑是扭曲的,因为在每个选项卡中都有一个超级奇怪的选项卡实现,但将来会改变。现在我只是在想是不是太多了,从道具中渲染出这么多东西(甚至要渲染的组件名称也在道具中动态传递)等等。
  • 好多了!我猜GenericTab 是主页包装内容吧?

标签: javascript inheritance reactjs


【解决方案1】:

在处理此类架构(即,您的案例中的选项卡)时,您基本上不想将架构隐藏在引擎盖下,因为在这种情况下,您最终会为每个您想要添加的新案例添加越来越多的属性处理。

相反,您不希望让 react 处理嵌套结构,因为它是 react 真正发挥作用的地方。通过处理内置的children 道具,您可以编写一些非常通用的东西。你通常想写这样的东西:

const PageWithTabs = (props) => (
  <Tabs defaultActive={'targaryens-panel'}>
    <TabBar>
      <TabLink href="#starks-panel">{'Starks'}</TabLink>
      <TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
      <TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
    </TabBar>
    <TabPanel id="starks-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Eddard</li>
        <li>Catelyn</li>
        <li>Robb</li>
        <li>Sansa</li>
        <li>Brandon</li>
        <li>Arya</li>
        <li>Rickon</li>
      </ul>
    </TabPanel>
    <TabPanel id="lannisters-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Tywin</li>
        <li>Cersei</li>
        <li>Jamie</li>
        <li>Tyrion</li>
      </ul>
    </TabPanel>
    <TabPanel id="targaryens-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Viserys</li>
        <li>Daenerys</li>
      </ul>
    </TabPanel>
  </Tabs>
)

这里的重点是,您不必“预测”每个TabPanel 下可能出现的所有内容,只需让开发人员随意放置即可! 但是我需要一些逻辑来处理“转到标签”之类的事情。

React 提供了一些非常方便的实用方法来动态克隆元素、映射元素和渲染元素,无论它的类型是否是你所期望的(在我们的例子中,我们期望 TabBarTabPanel 类型,没有什么可以阻止开发人员可以放置除这两个之外的任何其他组件,但没有什么能阻止他将任何内置的 &lt;table&gt; html 元素放在 &lt;a&gt; 标记内或类似的奇怪的东西)。

这是一个使用 Material Design Lite 的小实现,它并不完美,但你应该明白这一点:

class Tabs extends React.Component {
  constructor(props) {
    super(props),
    this.state = {
      activeTabId: props.defaultActive
    }
  }
  tabClickHandlerFactory(id) {
    return (e) => {
      e.preventDefault()
      this.setState({
        activeTabId: id
      })
    }
  }
  getPanelIdFromLink(href) {
      return href.split('#')[1]
  }
  render() {
    const self = this

    return (
      <div className='mdl-tabs is-upgraded' {...self.props}>
        {React.Children.map(self.props.children, (child, index) => {
          if (child.type == TabBar) {
            return React.cloneElement(child, {}, React.Children.map(child.props.children, (link) => {
              const id = self.getPanelIdFromLink(link.props.href)
              return (
                React.cloneElement(link, {
                    onClick: self.tabClickHandlerFactory(id),
                    active: self.state.activeTabId === id
                })
              )
            }))
          }
          if (child.type == TabPanel) {
            const { id } = child.props
            const active = self.state.activeTabId === id
            return active && React.cloneElement(child, { active: true })
          }
        })}
      </div>
    )
  }
}

Tabs.propTypes = {
  defaultActive: React.PropTypes.string.isRequired,
}

const TabBar = (props) => <div className='mdl-tabs__tab-bar' {...props}>{props.children}</div>

const TabLink = ({ active, ...props }) => {
  return (
    <a className={`mdl-tabs__tab${active ? ' is-active' : ''}`} {...props}>{props.children}</a>
  )
}


const TabPanel = ({ active, ...props }) => (
  <div className={`mdl-tabs__panel${active ? ' is-active' : ''}`} {...props}>{props.children}</div>
)

const PageWithTabs = (props) => (
  <Tabs defaultActive={'targaryens-panel'}>
    <TabBar>
      <TabLink href="#starks-panel">{'Starks'}</TabLink>
      <TabLink href="#lannisters-panel">{'Lannisters'}</TabLink>
      <TabLink href="#targaryens-panel">{'Targaryens'}</TabLink>
    </TabBar>
    <TabPanel id="starks-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Eddard</li>
        <li>Catelyn</li>
        <li>Robb</li>
        <li>Sansa</li>
        <li>Brandon</li>
        <li>Arya</li>
        <li>Rickon</li>
      </ul>
    </TabPanel>
    <TabPanel id="lannisters-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Tywin</li>
        <li>Cersei</li>
        <li>Jamie</li>
        <li>Tyrion</li>
      </ul>
    </TabPanel>
    <TabPanel id="targaryens-panel">
      <ul style={{ listStyleType: 'none' }}>
        <li>Viserys</li>
        <li>Daenerys</li>
      </ul>
    </TabPanel>
  </Tabs>
)

ReactDOM.render(<PageWithTabs/>, document.getElementById('app'))
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<link rel="stylesheet" href="https://code.getmdl.io/1.1.3/material.brown-orange.min.css" /> 
<div id='app'></div>

【讨论】:

    猜你喜欢
    • 2018-08-03
    • 2019-10-30
    • 2020-11-09
    • 2021-10-12
    • 1970-01-01
    • 1970-01-01
    • 2019-06-28
    • 2019-03-26
    • 2021-09-27
    相关资源
    最近更新 更多