【问题标题】:React HOC working on some but not other componentsReact HOC 在一些但不是其他组件上工作
【发布时间】: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;.,它仍然不起作用

标签: javascript reactjs react-hooks react-context recharts


【解决方案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() 的个人包装器。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-07-22
    • 1970-01-01
    • 1970-01-01
    • 2012-11-01
    • 2022-11-18
    • 1970-01-01
    • 2012-07-14
    相关资源
    最近更新 更多