输入一个 React 组件,克隆其子组件以在 TypeScript 中添加额外的道具
Typing a React component that clones its children to add extra props in TypeScript
假设您有以下组件接受一个或多个子 JSX.Elements 并在使用 React.cloneElement(child, { onCancel: () => {} }
.
渲染它们时将额外的回调传递给它们的道具
有问题的组件(摘录):
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
(父)级别传递的额外隐式道具?
您可以使用接口继承(参见 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 的所有属性,但使它们成为可选的。
这是不可能的。无法静态地知道 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
s
<XComponent onCancel={handleCancel} childrenAsFunction={props =>
<span>
<UserComponent {...props} />
<UserComponent {...props} />
</span>
} />
这会很好用,我经常使用这种模式,没有任何麻烦。
您可以重构 XComponentProps 以使用相关部分键入 childrenAsFunction
道具(此处的 onCancel
函数)。
即使在 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>
我知道这已经解决了,但另一个解决方案是限制可以传递的 children 的类型。限制包装器的功能,但可能是您想要的。
因此,如果您保留原始示例:
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>
);
更新:2021 年解决方案
运行 解决了这个问题,并找到了一个巧妙的解决方法:
将使用 Tabs 组件作为示例,但它是正在解决的相同问题(即在从父组件向子组件动态添加道具时保持 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;
假设您有以下组件接受一个或多个子 JSX.Elements 并在使用 React.cloneElement(child, { onCancel: () => {} }
.
有问题的组件(摘录):
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
(父)级别传递的额外隐式道具?
您可以使用接口继承(参见 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 的所有属性,但使它们成为可选的。
这是不可能的。无法静态地知道 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
s
<XComponent onCancel={handleCancel} childrenAsFunction={props =>
<span>
<UserComponent {...props} />
<UserComponent {...props} />
</span>
} />
这会很好用,我经常使用这种模式,没有任何麻烦。
您可以重构 XComponentProps 以使用相关部分键入 childrenAsFunction
道具(此处的 onCancel
函数)。
即使在 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>
我知道这已经解决了,但另一个解决方案是限制可以传递的 children 的类型。限制包装器的功能,但可能是您想要的。
因此,如果您保留原始示例:
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>
);
更新:2021 年解决方案
运行 解决了这个问题,并找到了一个巧妙的解决方法:
将使用 Tabs 组件作为示例,但它是正在解决的相同问题(即在从父组件向子组件动态添加道具时保持 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;