React HOC 在一些但不是其他组件上工作

Posted

技术标签:

【中文标题】React HOC 在一些但不是其他组件上工作【英文标题】:React HOC working on some but not other components 【发布时间】:2020-07-28 12:49:35 【问题描述】:

我正在使用 HOC 组件将操作绑定到许多不同类型的元素,包括 SVG 单元格,当 onClick 正常绑定时,它可以工作,但是当我使用我的 HOC 时,它会返回意外的结果。

最小可重现示例:https://codesandbox.io/s/ecstatic-keldysh-3viw0

HOC 组件:

export const withReport = Component => ( children, ...props ) => 
    console.log(Component); //this only prints for ListItem elements for some reason

    const  dispatch  = useContext(DashboardContext);

    const handleClick = () => 
        console.log('clicked!'); //even this wont work on some.
        const  report  = props;
        if (typeof report === "undefined") return false;

        dispatch( type: SET_ACTIVE_REPORT, activeReport: report );
        dispatch( type: TOGGLE_REPORT );
    ;

    return (
        <Component onClick=handleClick ...props>
            children
        </Component>
    );
;

使用工作:

const ListItemWIthReport = withReport(ListItem); //list item from react-mui
items.map((item, key) => (
    <ListItemWithReport report=item.report key=key button>
        /* listitem children*/
    </ListItemWithReport>
))

使用无效:

const BarWithReport = withReport(Bar); //Bar from recharts
bars.map((bar, index) => (
    <BarWithReport
        report=bar.report
        key=index
        dataKey=bar.name
        fill=bar.fill
    />
))

ListItem 按预期 100% 工作,但是,条形图不会在 BarChart 内呈现。同样,使用 PieChart 时,单元格实际上会根据它们的值呈现正确的大小,但是,像“填充”这样的道具似乎不会传递下去。

我是否错误地使用了 HOC?我在 Charts 内部没有看到除 HOC 之外的选项,因为许多类型的元素将被视为无效 html

【问题讨论】:

您如何在handleClick 中访问report @rzwnahmd 我的错,我删除了它上面的行,因为我认为我没有使用它,代码破坏了道具,让我更新。 我认为children 在渲染范围内无效,因为Component(HOC 的第一个参数)基本上已经是孩子了?这也是我可以(直观地)看到示例一和示例二之间的唯一区别。 @bbortt 是的,我也是这么想的,但是,即使你只返回&lt;Component onClick=handleClick ...props /&gt;.,它仍然不起作用 【参考方案1】:

您可能正在处理具有重要静态属性的组件,这些属性需要提升到包装的组件中,或者需要实现 ref 转发以便其父组件处理它们。将这些部件放在适当的位置很重要,尤其是在包装您不了解其内部结构的组件时。那个Bar 组件,例如does have some static properties。您的 HOC 正在让这些消失。

以下是提升这些静态成员的方法:

import hoistNonReactStatic from 'hoist-non-react-statics';

export const withReport = Component => 
  const EnhancedComponent = props => 
    const  dispatch  = useContext(DashboardContext);

    const handleClick = () => 
      const  report  = props;
      if (typeof report === "undefined") return false;

      dispatch( type: SET_ACTIVE_REPORT, activeReport: report );
      dispatch( type: TOGGLE_REPORT );
    ;

    return (
      <Component onClick=handleClick ...props/>
    );
  ;

  hoistNonReactStatic(EnhancedComponent, Component);
  return EnhancedComponent;
;

关于提升静态和引用转发的文档可以在这个handy guide to HOCs中找到。

可能有一些库可以为您处理所有这些细节。一个,addhoc,是这样工作的:

import addHOC from 'addhoc';

export const withReport = addHOC(render => 
  const  dispatch  = useContext(DashboardContext);

  const handleClick = () => 
    const  report  = props;
    if (typeof report === "undefined") return false;

    dispatch( type: SET_ACTIVE_REPORT, activeReport: report );
    dispatch( type: TOGGLE_REPORT );
  ;

  return render( onClick: handleClick );
);

当然,如果父组件通过类型显式检查子组件,那么你将根本无法使用 HOC。事实上,看起来 recharts 有这个问题。在这里你可以看到图表是defined in terms of child components,然后是searched for explicitly by type。

【讨论】:

您能否分叉最小可重现的示例并更新这些更改以显示它的工作原理?我已经在本地对提升非反应静态进行了更改,但没有产生任何影响。 我认为您是一种 SOL,因为 recharts 明确要求某些子组件类型,因此您的 HOC 不能用作交换替换。查看更新。我有解决方法的想法,请继续关注... 谢谢,我已经在 recharts 仓库上打开了一个问题,看看维护者是否还有其他要添加的内容。 所以我有 5 个不同的图表元素,需要将 onClick 绑定到一个系列,并且由于 HOC 不起作用,对于最小的代码复制,您有什么建议?【参考方案2】:

我认为您的 HOC 无效,因为并非每个包装组件(例如 HTML 元素)基本上都是可点击的。也许这个片段可以澄清我想说的:

const withReport = Component => (props) => 
  const handleClick = () => console.log('whatever')

  // Careful - your component might not support onClick by default
  return <Component onClick=handleClick  ...props />
  // vs.
  return <div onClick=handleClick style=backgroundColor: 'green'>
    <Component ...props />

    props.children
  </div>


// Your import from wherever you want
class SomeClass extends React.Component 
  render() 
    return <span onClick=this.props.onClick>this.props.children</span>
    // vs.
    return <span style=backgroundColor: 'red'>
      
        // Careful - your imported component might not support children by default
        this.props.children
      
    </span>
  


const ReportedListItem = withReport(SomeClass)

ReactDOM.render(<ReportedListItem>
  <h2>child</h2>
</ReportedListItem>, mountNode)

你可以有上部或下部(由vs. 分隔)但不能交叉。使用第二个返回(受控包装组件)的 HOC 肯定会更节省。

【讨论】:

“当一个onClick正常绑定时,它工作”,也不能用div包装组件,因为div不是SVG的有效子。【参考方案3】:

我已经成功使用了 4 种方法来包装 Recharts 组件。

第一种方法

将组件包装在 HOC 中,并使用带有一些重载的 Object.Assign。这会破坏一些动画并且难以在线上使用活动点。 Recharts 在渲染组件之前会从组件中获取一些道具。所以如果 prop 没有传递到 HOC 中,那么它就不会正确渲染。

...

function LineWrapper(
  dataOverload,
  data,
  children,
  strokeWidth,
  strokeWidthOverload,
  isAnimationActive,
  dot,
  dotOverload,
  activeDot,
  activeDotOverload,
  ...rest
: PropsWithChildren<Props>) 
  const defaultDotStroke = 12;

  return (
    <Line
      aria-label="chart-line"
      isAnimationActive=false
      strokeWidth=strokeWidthOverload ?? 2
      data=dataOverload?.chartData ?? data
      dot=dotOverload ??  strokeWidth: defaultDotStroke 
      activeDot=activeDotOverload ??  strokeWidth: defaultDotStroke + 2 
      ...rest
    >
      children
    </Line>
  );

export default renderChartWrapper(Line, LineWrapper, 
  activeDot: <Dot r=14 />,
);
const renderChartWrapper = <P extends BP, BP = >(
  component: React.ComponentType<BP>,
  wrapperFC: React.FC<P>,
  defaultProps?: Partial<P>
): React.FC<P> => 
  Object.assign(wrapperFC, component);

  if (defaultProps) 
    wrapperFC.defaultProps = wrapperFC.defaultProps ?? ;
    Object.assign(wrapperFC.defaultProps, defaultProps);
  

  return wrapperFC;
;

第二种方法


使用默认道具分配值。任何传入 HOC 的 props 都会被覆盖。

import  XAxisProps  from 'recharts';

import  createStyles  from '@material-ui/core';

import  themeExtensions  from '../../../assets/theme';

const useStyles = createStyles(
  tickStyle: 
    ...themeExtensions.font.graphAxis,
  ,
);

type Props = XAxisProps;

// There is no actual implementation of XAxis. Recharts render function grabs the props only.

function XAxisWrapper(props: Props) 
  return null;


XAxisWrapper.displayName = 'XAxis';
XAxisWrapper.defaultProps = 
  allowDecimals: true,
  hide: false,
  orientation: 'bottom',
  width: 0,
  height: 30,
  mirror: false,
  xAxisId: 0,
  type: 'category',
  domain: [0, 'auto'],
  padding:  left: 0, right: 0 ,
  allowDataOverflow: false,
  scale: 'auto',
  reversed: false,
  allowDuplicatedCategory: false,
  tick:  style: useStyles.tickStyle ,
  tickCount: 5,
  tickLine: false,
  dataKey: 'key',
;

export default XAxisWrapper;

第三种方法


我不喜欢这个,所以我已经解决了这个问题,但是你可以扩展这个类。

export default class LineWrapper extends Line 

 render()
  return (<Line ...this.props />
 

第四种方法


我没有一个简单的例子,但我总是渲染形状或孩子并提供帮助的功能。例如,对于条形单元格,我使用这个:

export default function renderBarCellPattern(cellOptions: CellRenderOptions) 
  const  data, fill, match, pattern  = cellOptions;
  const id = _uniqueId();
  const cells = data.map((d) =>
    match(d) ? (
      <Cell
        key=`cell-$id`
        strokeWidth=4
        stroke=fill
        fill=`url(#bar-mask-pattern-$id)`
      />
    ) : (
      <Cell key=`cell-$id` strokeWidth=2 fill=fill />
    )
  );

  return !pattern
    ? cells
    : cells.concat(
        <CloneElement<MaskProps>
          key=`pattern-$id`
          element=pattern
          id=`bar-mask-pattern-$id`
          fill=fill
        />
      );


// and

<Bar ...requiredProps>
renderBarCellPattern(...cell details)
</Bar>

CloneElement 只是 Reacts cloneElement() 的个人包装器。

【讨论】:

以上是关于React HOC 在一些但不是其他组件上工作的主要内容,如果未能解决你的问题,请参考以下文章

react-高阶组件-Hoc

在相邻组件之间共享内容,HOC?

在带有装饰器和 HOC 的类组件中使用 react-i18next

React: 高阶组件(HOC)

在 HoC 中使用反应钩子时的警告

React - 何时使用钩子,何时使用 HOC