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() 的个人包装器。
我正在使用 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() 的个人包装器。