【问题标题】:Typing a React component that clones its children to add extra props in TypeScript键入一个克隆其子级的 React 组件以在 TypeScript 中添加额外的道具
【发布时间】:2017-06-12 10:09:19
【问题描述】:

假设您有以下组件,该组件接受一个或多个子 JSX.Elements,并在使用 React.cloneElement(child, { onCancel: () => {} } 渲染它们时将额外的回调传递给它们的 props。

有问题的组件(摘录):

interface XComponentProps {
    onCancel: (index: number) => void;
}

class XComponent extends React.Component<XComponentProps> {
    render() {
        const { onCancel } = this.props;
        const children = typeof this.props.children === 'function' ?
            React.cloneElement(this.props.children, { onCancel: () => onCancel() }) :
            this.props.children.map((child, idx) => (
                React.cloneElement(child, { onCancel: () => onCancel(idx) })
            ));
        return <div>{children}</div>;
    }
}

正在使用的相关组件(摘录):

interface UserComponentProps { caption: string }
const UserComponent = (props: UserComponentProps) => (
    <button onClick={props.onClose}>{props.caption || "close"}</button>
);

ReactDOM.render(
    <XComponent onCancel={handleCancel}>
        <UserComponent />
        <UserComponent />
        <UserComponent />
    </XComponent>
);

现在 TSC 抱怨 UserComponent 在其 props 的接口定义中没有 onCancel,实际上没有。一种最简单的解决方法是手动将onCancel 定义为UserComponentProps 接口。

但是,我想在不修改子节点的 prop 定义的情况下修复它,以便组件可以接受任意一组 React 元素。在这种情况下,有没有办法定义返回元素的类型,这些元素在XComponent(父)级别传递了额外的隐式道具?

【问题讨论】:

    标签: javascript reactjs typescript typescript-typings


    【解决方案1】:

    更新:2021 年解决方案

    遇到了这个问题,后来想出了一个巧妙的解决方法:

    将使用 Tabs 组件作为示例,但解决的问题是相同的(即,在从父组件向子组件动态添加 props 时保持 TS 快乐)。


    components/Tabs/Tab.tsx

    // Props to be passed in via instantiation in JSX
    export interface TabExternalProps {
      heading: string;
    }
    
    // Props to be passed in via React.Children.map in parent <Tabs /> component
    interface TabInternalProps extends TabExternalProps {
      index: number;
      isActive: boolean;
      setActiveIndex(index: number): void;
    }
    
    export const Tab: React.FC<TabInternalProps> = ({
      index,
      isActive,
      setActiveIndex,
      heading,
      children
    }) => {
      const className = `tab ${isActive && 'tab--active'}`;
      return (
        <div onClick={() => setActiveIndex(index)} {...className}>
          <strong>{heading}</strong>
          {isActive && children}
        </div>
      )
    }
    

    components/Tabs/Tabs.tsx

    import { Tab, TabExternalProps } from './Tab'; 
    
    interface TabsProps = {
      defaultIndex?: number;
    }
    interface TabsComposition = {
      Tab: React.FC<TabExternalProps>;
    }
    
    const Tabs: React.FC<TabsProps> & TabsComposition = ({ children, defaultIndex = 0 }) => {
      const [activeIndex, setActiveIndex] = useState<number>(defaultActiveIndex);
      const childrenWithProps = React.Children.map(children, (child, index) => {
        if (!React.isValidElement(child)) return child;
        const JSXProps: TabExternalProps = child.props;
        const isActive = index === activeIndex;
        return (
          <Tab
            heading={JSXProps.heading}
            index={index}
            isActive={isActive}
            setActiveIndex={setActiveIndex}
          >
            {React.cloneElement(child)}
          </Tab>
        )
      })
      return <div className='tabs'>{childrenWithProps}</div>
    }
    
    Tabs.Tab = ({ children }) => <>{children}</>
    
    export { Tabs }
    

    App.tsx

    import { Tabs } from './components/Tabs/Tabs';
    
    const App: React.FC = () => {
      return(
        <Tabs defaultIndex={1}>
          <Tabs.Tab heading="Tab One">
            <p>Tab one content here...</p>
          </Tab>
          <Tabs.Tab heading="Tab Two">
            <p>Tab two content here...</p>
          </Tab>
        </Tabs>
      )
    }
    
    export default App;
    

    【讨论】:

    • 这与 OP 的问题不同 - 当您 React.cloneElement(child) 时,您不会将任何道具传递给孩子。
    【解决方案2】:

    我知道这已经解决了,但是另一个解决方案是限制可以传递的孩子的类型。限制包装器的功能,但可能是您想要的。

    所以如果你保留原来的例子:

    interface XComponentProps {
       onCancel: (index: number) => void;
       children: ReactElement | ReactElement[]
    }
    
    class XComponent extends React.Component<XComponentProps> {
        render() {
            const { onCancel } = this.props;
            const children = !Array.isArray(this.props.children) ?
                React.cloneElement(this.props.children, { onCancel: () => onCancel() }) :
                this.props.children.map((child, idx) => (
                    React.cloneElement(child, { onCancel: () => onCancel(idx) })
                ));
            return <div>{children}</div>;
        }
    }
    
    interface UserComponentProps { caption: string }
    const UserComponent: React.FC = (props: UserComponentProps) => (
        <button onClick={props.onClose}>{props.caption || "close"}</button>
    );
    
    ReactDOM.render(
        <XComponent onCancel={handleCancel}>
            <UserComponent />
            <UserComponent />
            <UserComponent />
        </XComponent>
    );
    

    【讨论】:

      【解决方案3】:

      即使在 JSX 中,您也可以将子代作为函数传递。这样,您将一路正确打字。 UserComponents 道具接口应该扩展 ChildProps。

      interface ChildProps {
          onCancel: (index: number) => void;
      }
      
      interface XComponentProps {
          onCancel: (index: number) => void;
          children: (props: ChildProps) => React.ReactElement<any>;
      }
      
      class XComponent extends React.Component<XComponentProps> {
          render() {
              const { onCancel } = this.props;
              return children({ onCancel })};
          }
      }
      
      
      <XComponent onCancel={handleCancel}>
          { props => <UserComponent {...props} /> } 
      </XComponent>
      

      【讨论】:

        【解决方案4】:

        这是不可能的。在你的 ReactDOM.render 上下文中,没有办法静态知道 UserComponent 从父 XComponent 收到什么道具。

        如果您想要一个类型安全的解决方案,请使用子函数:

        这是XComponent的定义

        interface XComponentProps {
            onCancel: (index: number) => void;
            childrenAsFunction: (props: { onCancel: (index: number) => void }) => JSX.Element;
        }
        
        class XComponent extends React.Component<XComponentProps> {
            render() {
                const { onCancel } = this.props;
                return <div>{childrenAsFunction({ onCancel })}</div>;
            }
        }
        

        现在你可以用它来渲染你的UserComponents

        <XComponent onCancel={handleCancel} childrenAsFunction={props => 
          <span>
            <UserComponent {...props} />
            <UserComponent {...props} />
          </span>
        } />
        

        这会很好地工作,我经常使用这种模式,没有麻烦。 您可以重构 XComponentProps 以键入带有相关部分的 childrenAsFunction 道具(此处为 onCancel 函数)。

        【讨论】:

        • 感谢有趣的解决方案!作为函数传递是有意义的,这样 TSC 就可以知道父子关系。
        【解决方案5】:

        您可以使用接口继承(参见Extending Interfaces)并让 UserComponentProps 扩展 XComponentProps:

        interface UserComponentProps extends XComponentProps { caption: string }

        这将为 UserComponentProps 提供 XComponentProps 的所有属性以及它自己的属性。

        如果您不想要求 UserComponentProps 定义 XComponentProps 的所有属性,您也可以使用 Partial 类型(参见Mapped Types):

        interface UserComponentProps extends Partial&lt;XComponentProps&gt; { caption: string }

        这将为 UserComponentProps 提供 XComponentProps 的所有属性,但使它们成为可选的。

        【讨论】:

        • 感谢@erikamjoh,这是我目前的解决方法,但我想在不修改子组件道具的情况下实现它...
        猜你喜欢
        • 2019-01-18
        • 2019-10-18
        • 2021-06-25
        • 2018-03-08
        • 2017-10-15
        • 2018-10-14
        • 2018-09-24
        • 2022-01-07
        • 1970-01-01
        相关资源
        最近更新 更多