[英]Inject children into react component passed by props in 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
(父)級別傳遞的額外隱式道具?
這不可能。 沒有辦法靜態地知道 UserComponent 從 ReactDOM.render 上下文中的父 XComponent 接收到什么 props。
如果您想要一個類型安全的解決方案,請使用 children 作為函數:
這是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 以使用相關部分(此處為onCancel
函數)鍵入childrenAsFunction
道具。
您可以使用接口繼承(請參閱擴展接口)並讓 UserComponentProps 擴展 XComponentProps:
interface UserComponentProps extends XComponentProps { caption: string }
這將為 UserComponentProps 提供 XComponentProps 的所有屬性以及它自己的屬性。
如果您不想要求 UserComponentProps 定義 XComponentProps 的所有屬性,您還可以使用 Partial 類型(請參閱Mapped Types ):
interface UserComponentProps extends Partial<XComponentProps> { caption: string }
這將為 UserComponentProps 提供 XComponentProps 的所有屬性,但使它們成為可選的。
即使在 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>
我知道這已經解決了,但另一個解決方案是限制可以傳遞的孩子的類型。 限制包裝器的功能,但可能是您想要的。
因此,如果您保留原始示例:
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>
);
遇到了這個問題,后來想出了一個巧妙的解決方法:
將使用 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;
正如@Benoit B. 所說:
這不可能。 沒有辦法靜態地知道 UserComponent 在 ReactDOM.render 上下文中從父 XComponent 接收到什么道具。
也就是說,有一種替代方法可以實現這一點,即使用高階組件來包裝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'>;
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.