添加道具类型以渲染道具功能
add prop types to render props function
当使用常规的 React 组件时,有多种方法可以为组件启用智能感知(通过功能组件的属性类型,或 jsdoc 注释),但如果组件使用这样的渲染属性模式:
return this.props.children({ config, onClickConfig })
并像这样使用:
<ConsumerComponent>
{
({config, onClickConfig}) => (<button type="button" onClick={onClickConfig}>{config}</button>)
}
</ConsumerComponent>
有什么方法可以为 config
对象或 onClickConfig
函数的类型启用智能感知吗?
我看到打字稿可以通过重用消费者中的类型来实现这一点,但是是否可以使用 jsdocs 或 prop 类型来实现?
我的猜测是同时使用两者并妥善记录。
const propTypes = {
children: PropTypes.func.isRequired,
// ... other proptypes
}
/**
* @param {object} props the props object
* @param {({ config: React.ElementType, onClickConfig: function }) => React.ElementType} props.children the render prop
*/
const ConsumerComponent = ({ children, ...props }) => ( ..... );
ConsumerComponent.propTypes = propTypes;
问题
不幸的是,PropTypes
不允许在运行时对函数的(render prop)参数进行类型检查。如果您使用的是 Typescript,那么无效的参数类型应该会阻止构建期间的编译(前提是 noEmitOnError 设置为 true)。但是,由于您尝试在运行时键入,因此您必须使用自己的自定义验证器函数来完成。
解决方案
单击 Open Modal
按钮会引发错误。要更正错误,请将 Line 72 更改为 boolean
.
虽然这个例子非常随意,因为渲染道具没有暴露,如果你要将 isOpen
状态提升到 App
组件,那么它仍然需要检查isOpen
Modal
组件中的属性类型是布尔值(使用 PropTypes
或您的自定义验证器)。
代码
App.js
import * as React from "react";
import Modal from "./Modal";
import "uikit/dist/css/uikit.min.css";
import "./styles.css";
const App = (): React.ReactElement => (
<div className="app">
<Modal>
{({ isOpen, toggleModal }): React.ReactElement => (
<>
<h1 className="title">Hello!</h1>
<p className="subtitle">
The modal is currently {isOpen ? "visible" : "hidden"}!
</p>
<p className="subtitle">There are two ways to close this modal:</p>
<ul>
<li>Click outside of this modal in the grey overlay area.</li>
<li>Click the close button below.</li>
</ul>
<button
className="uk-button uk-button-danger uk-button-small"
onClick={toggleModal}
>
Close
</button>
</>
)}
</Modal>
</div>
);
export default App;
Modal.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import PropTypes from "prop-types";
import withTypeCheck from "./withTypeCheck";
export type ChildrenProps = {
isOpen: boolean;
toggleModal: () => void;
};
export type ChildFunc = ({
isOpen,
toggleModal
}: ChildrenProps) => React.ReactElement;
export type ModalProps = {
children: ChildFunc;
};
/**
* Reuseable Modal component
*
* @param children - a render prop that accepts an object of `isOpen` and `toggleModal` props
* @returns a ReactElement
* @example <Modal>({ isOpen, toggleModal }) => (...)}</Modal>
*/
const Modal = ({ children }: ModalProps): React.ReactElement => {
const [isOpen, setVisible] = React.useState(false);
const wrapperRef = React.useRef<HTMLDivElement>(null);
const toggleModal = React.useCallback(() => {
setVisible((prevState) => !prevState);
}, []);
const closeModal = React.useCallback(
(e: Event): void => {
if (
isOpen &&
wrapperRef.current &&
!wrapperRef.current.contains(e.target as Node)
) {
toggleModal();
}
},
[isOpen, toggleModal]
);
React.useEffect(() => {
document.addEventListener("click", closeModal, { capture: true });
return () => {
document.removeEventListener("click", closeModal, { capture: true });
};
}, [closeModal]);
return (
<>
<button
className="uk-button uk-button-primary uk-button-small"
type="button"
onClick={toggleModal}
>
Open Modal
</button>
{isOpen &&
ReactDOM.createPortal(
<>
<div className="overlay" />
<div className="window-container">
<div className="modal-container">
<div ref={wrapperRef} className="modal">
{withTypeCheck(children, { isOpen: "true", toggleModal })}
</div>
</div>
</div>
</>,
document.body
)}
</>
);
};
Modal.propTypes = {
children: PropTypes.func.isRequired
};
export default Modal;
withTypeCheck.ts(可以换成console.error
抛出错误,但抛出会更明显)
import type { ChildrenProps, ChildFunc } from "./Modal";
/**
* Type checks a render prop function and its arguments.
*
* @param children - a render prop function
* @args - an object of arugments containing `isOpen` and `toggleModal` props
* @returns an error or ReactElement
* @example withTypeCheck(fn, args);
*/
const withTypeCheck = (
children: Function,
args: ChildrenProps
): Error | ChildFunc => {
try {
const childrenType = typeof children;
if (childrenType !== "function")
throw String(
`An invalid children prop was used inside the Modal component. Expected a function, but received ${childrenType}.`
);
const argTypes = typeof args;
if (argTypes !== "object")
throw String(
`Invalid arguments were supplied to the children prop. Expected an object, but received ${argTypes}.`
);
const isOpenType = typeof args.isOpen;
if (isOpenType !== "boolean")
throw String(
`An invalid 'isOpen' argument was supplied to the Modal's children prop. Expected a boolean, but received a ${isOpenType}.`
);
const toggleModalType = typeof args.toggleModal;
if (toggleModalType !== "function")
throw String(
`An invalid 'toggleModalType' was argument supplied to the Modal's children prop. Expected a function, but received a ${toggleModalType}.`
);
return children(args);
} catch (error) {
throw error;
}
};
export default withTypeCheck;
index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);
当使用常规的 React 组件时,有多种方法可以为组件启用智能感知(通过功能组件的属性类型,或 jsdoc 注释),但如果组件使用这样的渲染属性模式:
return this.props.children({ config, onClickConfig })
并像这样使用:
<ConsumerComponent>
{
({config, onClickConfig}) => (<button type="button" onClick={onClickConfig}>{config}</button>)
}
</ConsumerComponent>
有什么方法可以为 config
对象或 onClickConfig
函数的类型启用智能感知吗?
我看到打字稿可以通过重用消费者中的类型来实现这一点,但是是否可以使用 jsdocs 或 prop 类型来实现?
我的猜测是同时使用两者并妥善记录。
const propTypes = {
children: PropTypes.func.isRequired,
// ... other proptypes
}
/**
* @param {object} props the props object
* @param {({ config: React.ElementType, onClickConfig: function }) => React.ElementType} props.children the render prop
*/
const ConsumerComponent = ({ children, ...props }) => ( ..... );
ConsumerComponent.propTypes = propTypes;
问题
不幸的是,PropTypes
不允许在运行时对函数的(render prop)参数进行类型检查。如果您使用的是 Typescript,那么无效的参数类型应该会阻止构建期间的编译(前提是 noEmitOnError 设置为 true)。但是,由于您尝试在运行时键入,因此您必须使用自己的自定义验证器函数来完成。
解决方案
单击 Open Modal
按钮会引发错误。要更正错误,请将 Line 72 更改为 boolean
.
虽然这个例子非常随意,因为渲染道具没有暴露,如果你要将 isOpen
状态提升到 App
组件,那么它仍然需要检查isOpen
Modal
组件中的属性类型是布尔值(使用 PropTypes
或您的自定义验证器)。
代码
App.js
import * as React from "react";
import Modal from "./Modal";
import "uikit/dist/css/uikit.min.css";
import "./styles.css";
const App = (): React.ReactElement => (
<div className="app">
<Modal>
{({ isOpen, toggleModal }): React.ReactElement => (
<>
<h1 className="title">Hello!</h1>
<p className="subtitle">
The modal is currently {isOpen ? "visible" : "hidden"}!
</p>
<p className="subtitle">There are two ways to close this modal:</p>
<ul>
<li>Click outside of this modal in the grey overlay area.</li>
<li>Click the close button below.</li>
</ul>
<button
className="uk-button uk-button-danger uk-button-small"
onClick={toggleModal}
>
Close
</button>
</>
)}
</Modal>
</div>
);
export default App;
Modal.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import PropTypes from "prop-types";
import withTypeCheck from "./withTypeCheck";
export type ChildrenProps = {
isOpen: boolean;
toggleModal: () => void;
};
export type ChildFunc = ({
isOpen,
toggleModal
}: ChildrenProps) => React.ReactElement;
export type ModalProps = {
children: ChildFunc;
};
/**
* Reuseable Modal component
*
* @param children - a render prop that accepts an object of `isOpen` and `toggleModal` props
* @returns a ReactElement
* @example <Modal>({ isOpen, toggleModal }) => (...)}</Modal>
*/
const Modal = ({ children }: ModalProps): React.ReactElement => {
const [isOpen, setVisible] = React.useState(false);
const wrapperRef = React.useRef<HTMLDivElement>(null);
const toggleModal = React.useCallback(() => {
setVisible((prevState) => !prevState);
}, []);
const closeModal = React.useCallback(
(e: Event): void => {
if (
isOpen &&
wrapperRef.current &&
!wrapperRef.current.contains(e.target as Node)
) {
toggleModal();
}
},
[isOpen, toggleModal]
);
React.useEffect(() => {
document.addEventListener("click", closeModal, { capture: true });
return () => {
document.removeEventListener("click", closeModal, { capture: true });
};
}, [closeModal]);
return (
<>
<button
className="uk-button uk-button-primary uk-button-small"
type="button"
onClick={toggleModal}
>
Open Modal
</button>
{isOpen &&
ReactDOM.createPortal(
<>
<div className="overlay" />
<div className="window-container">
<div className="modal-container">
<div ref={wrapperRef} className="modal">
{withTypeCheck(children, { isOpen: "true", toggleModal })}
</div>
</div>
</div>
</>,
document.body
)}
</>
);
};
Modal.propTypes = {
children: PropTypes.func.isRequired
};
export default Modal;
withTypeCheck.ts(可以换成console.error
抛出错误,但抛出会更明显)
import type { ChildrenProps, ChildFunc } from "./Modal";
/**
* Type checks a render prop function and its arguments.
*
* @param children - a render prop function
* @args - an object of arugments containing `isOpen` and `toggleModal` props
* @returns an error or ReactElement
* @example withTypeCheck(fn, args);
*/
const withTypeCheck = (
children: Function,
args: ChildrenProps
): Error | ChildFunc => {
try {
const childrenType = typeof children;
if (childrenType !== "function")
throw String(
`An invalid children prop was used inside the Modal component. Expected a function, but received ${childrenType}.`
);
const argTypes = typeof args;
if (argTypes !== "object")
throw String(
`Invalid arguments were supplied to the children prop. Expected an object, but received ${argTypes}.`
);
const isOpenType = typeof args.isOpen;
if (isOpenType !== "boolean")
throw String(
`An invalid 'isOpen' argument was supplied to the Modal's children prop. Expected a boolean, but received a ${isOpenType}.`
);
const toggleModalType = typeof args.toggleModal;
if (toggleModalType !== "function")
throw String(
`An invalid 'toggleModalType' was argument supplied to the Modal's children prop. Expected a function, but received a ${toggleModalType}.`
);
return children(args);
} catch (error) {
throw error;
}
};
export default withTypeCheck;
index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById("root")
);