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

React HOC working on some but not other components

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

可重现性最低的示例: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,Cells 将实际呈现,根据它们的值具有正确的大小,但是,像 "fill" 这样的道具似乎不会传递下去。

我是不是用错了 HOC?除了 HOC 之外,我在图表内部看不到其他选项,因为许多类型的元素将被视为无效 HTML?

我认为您的 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. 分隔)但不能交叉。使用第二个return(controlled wrapper-Component)的HOC肯定更节省。

您可能正在处理具有重要静态属性的组件,这些属性需要提升到包装组件中,或者需要实现引用转发以便其父组件处理它们。让这些部分就位很重要,尤其是在包装您不知道其内部结构的组件时。 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;
};

有关提升静力学和 ref 转发的文档可以在此 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 which are then searched for explicitly by type.

我已经成功地使用了 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 的任何道具都将被覆盖。

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;

第三种方法


我不喜欢这个,所以我解决了它,但你可以扩展 class。

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() 的个人包装器。