reactjs 在TypeScript中键入一个React组件,该组件克隆其子组件以添加额外的 prop

umuewwlo  于 2023-01-12  发布在  React
关注(0)|答案(6)|浏览(165)

假设您有以下组件,该组件接受一个或多个子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(父节点)级别传递额外的隐式prop?

a6b3iqyw

a6b3iqyw1#

这是不可能的。没有办法静态地知道UserComponent从ReactDOM.render上下文中的父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>;
    }
}

现在您可以使用它来渲染UserComponent

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

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

yptwkmov

yptwkmov2#

您可以使用接口继承(参见Extending Interfaces)并让UserComponentProps扩展XComponentProps:
interface UserComponentProps extends XComponentProps { caption: string }
这将给予UserComponentProps提供XComponentProps的所有属性以及它自己的属性。
如果你不想让UserComponentProps定义XComponentProps的所有属性,你也可以使用Partial类型(参见Mapped Types):
interface UserComponentProps extends Partial<XComponentProps> { caption: string }
这将给予UserComponentProps提供XComponentProps的所有属性,但使它们成为可选属性。

dvtswwa3

dvtswwa33#

你可以把child作为一个函数来传递,即使在JSX中也是如此。这样你就可以得到正确的输入。UserComponents props接口应该扩展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>
qcbq4gxm

qcbq4gxm4#

正如@Benoit B.所说:
这是不可能的。没有办法静态地知道UserComponent从ReactDOM.render上下文中的父XComponent接收到什么属性。
也就是说,有一种替代方法可以实现这一点,即使用高阶分量来 Package UserComponent(不修改它,如果有意义,可以重命名):

type RawUserComponentProps = Pick<XComponentProps, 'onCancel'> & {
  caption: string;
};

const RawUserComponent = (props: RawUserComponentProps) => {
  return <>...</>;
};

type UserComponentProps = Omit<RawUserComponentProps, 'onCancel'> & Pick<Partial<RawUserComponentProps>, 'onCancel'>;

export const UserComponent = (props: RawUserComponentProps) => {
  if (props.onCancel == null) {
    throw new Error('You must provide onCancel property');
  }
  return <RawUserComponent {...props} />;
};

您还可以将Omit & Pick简化为MakeOptional类型帮助器:

type MakeOptional<TObject, TKeys extends keyof TObject> = Omit<TObject, TKeys> & Pick<Partial<TObject>, TKeys>;

type UserComponentProps = MakeOptional<RawUserComponentProps, 'onCancel'>;
edqdpe6u

edqdpe6u5#

我知道这个问题已经解决了,但是另一个解决方案是限制可以传递的子元素的类型。限制 Package 器的功能,但是可能是您想要的。
所以如果你保留原来的例子:

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>
);
4zcjmb1e

4zcjmb1e6#

更新:解决方案2021

遇到此问题后,我找到了一个巧妙的解决方法:

将使用标签组件作为一个例子,但它是相同的问题正在解决(即保持TS高兴时,动态添加 prop 到子组件,从父组件)。

一个月一次
// 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>
  )
}
一米一分一秒
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;

相关问题