有没有一个好的方法可以在回调或渲染道具中使用React钩子



我经常发现自己在创建或使用组件,其中回调用于创建组件树的一部分,如以下两个简化示例:

// Example 1: Render an array of components
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <li key={item.id} style={{ paddingTop: item.paddingTop }}>{item.label}</li>)}
</ul>
);
// Example 2: Use render props
const ExampleComponent2 = () => (
<Dialog
renderHeader={useCallback((dialogCtx) => 'Header', [])}
renderBody={useCallback((dialogCtx) => <button onClick={() => dialogCtx.closeDialog(null)}>Test</button>, [])}
/>
);

正如您所看到的,这两种情况下的回调结果都取决于回调参数。为了避免在每次渲染时创建新的style对象(在示例1中(和新的onClick函数(在示例2中(,我应该将它们封装在useMemo/useCallback中,但这在这里是不可能的,因为它们是在回调中创建的,而不是在组件的根级别上创建的。

我知道解决这个问题的唯一方法是为每个回调创建指定的组件:

// Example 1: Render an array of components
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <ExampleComponent1Item key={item.id} item={item}/>)}
</ul>
);
const ExampleComponent1Item = ({ item }) => <li style={useMemo(() => ({ paddingTop: item.paddingTop }), [item.paddingTop])}>{item.label}</li>;

// Example 2: Use render props
const ExampleComponent2 = () => (
<Dialog
renderHeader={useCallback((dialogCtx) => <ExampleComponent2Header dialogCtx={dialogCtx}/>, [])}
renderBody={useCallback((dialogCtx) => <ExampleComponent2Body dialogCtx={dialogCtx}/>, [])}
/>
);
const ExampleComponent2Header = ({ dialogCtx }) => 'Header';
const ExampleComponent2Body = ({ dialogCtx }) => <button onClick={useCallback(() => dialogCtx.closeDialog(null), [dialogCtx.closeDialog])}>Test</button>;

你已经可以在这个简化的例子中看到,以这种方式拆分应用程序会产生大量额外的代码,并使应用程序更难阅读。在更复杂的场景中,父组件中的许多道具需要在子组件中重用,子组件将变得更加庞大,尤其是当道具类型也需要定义时。

在我看来,这两个例子都是相当常见的用例,因为回调是从React中的数组生成组件列表的唯一方法,而渲染道具是更复杂组件中的常见模式。我想知道:

  • 有没有办法在不将回调结果拆分为单独组件的情况下编写上面的示例?例如,在回调中直接使用钩子的方法
  • 是否有一种替代模式可以将数组映射到不依赖回调的组件列表,从而可以使用钩子
  • 有没有一种替代模式来渲染道具,使其更容易使用钩子

更新:为了明确起见,我正在寻找一种编程模式,而不是上面简化示例代码中的特定问题。

我不明白示例1出了什么问题。

const ExampleComponent1 = () => (
<ul>
{items.map((item) => <li key={item.id} style={{ paddingTop: item.paddingTop }}>{item.label}</li>)}
</ul>
);

这在React中非常好。在你看到style道具导致性能问题之前,你不应该试图优化它

带挂钩的版本,以防你真的需要它:

const ExampleComponent1Item = ({ item }) => {
const style = useMemo(() => ({ paddingTop: item.paddingTop }), [item.paddingTop]);
return (<li style={style}>{item.label}</li>);
};
const ExampleComponent1 = () => (
<ul>
{items.map((item) => <ExampleComponent1Item key={item.id} item={item}/>)}
</ul>
);

示例2:也是如此

const ExampleComponent2 = () => {
const renderHeader = useCallback((dialogCtx) => 'Header', []);
const renderBody = useCallback((dialogCtx) => <button onClick={() => dialogCtx.closeDialog(null)}>Test</button>, []);
return (<Dialog renderHeader={renderHeader} renderBody=renderBody}/>);
};

如果您想避免每次调用renderBody时重新创建onClick函数,可以执行以下操作:

const ExampleComponent2Body = {( dialogCtx }) => {
const onClick = useCallback(() => dialogCtx.closeDialog(null), [dialogCtx.closeDialog]);
return (<button onClick={onClick}>Test</button>);
};
const ExampleComponent2 = () => {
const renderHeader = useCallback((dialogCtx) => 'Header', []);
const renderBody = useCallback((dialogCtx) => <ExampleComponent2Body dialogCtx={dialogCtx}/>, []);
return (<Dialog renderHeader={renderHeader} renderBody=renderBody}/>);
};