React:不使用挂钩的基于 class 的组件中的无效挂钩
React : Invalid hook in a class-based component that doesn't use a hook
我对 React 有一些疑问。我想创建一个 class 形式的组件库(我来自 Java 世界,我发现 OOP 更具可读性)。
为此,我创建了一个基础 class,我的所有组件都将从该基础继承。
import { Box, createMuiTheme, Theme } from '@material-ui/core';
import React from 'react';
import { findDOMNode } from 'react-dom';
import { MyTheme, defaultPalette } from '../MyTheme/MyTheme';
import { BoxProps } from './BoxProps';
import { MyComponentProperties, MyComponentState } from './MyComponent.types';
import { ComponentUtilities } from './ComponentUtilities';
import { EventListenerPool } from './EventListenerPool';
/**
* Default class for create a Component with this library
* @abstract
*/
export abstract class
MyComponent<P extends MyComponentProperties, S extends MyComponentState>
extends React.Component<P, S> {
static readonly VERSION: number = 100;
/**
* Constant for the event : componentDidMount
* @constant EVENT_COMPONENT_DID_MOUNT
*/
protected static readonly EVENT_COMPONENT_DID_MOUNT = 'EVENT_COMPONENT_DID_MOUNT';
/**
* Constant for the event : componentWillUnmount
* @constant EVENT_COMPONENT_WILL_UNMOUNT
*/
protected static readonly EVENT_COMPONENT_WILL_UNMOUNT = 'EVENT_COMPONENT_WILL_UNMOUNT';
/**
* Constant for the event : clickOutside
* @constant EVENT_COMPONENT_CLICK_OUTSIDE
*/
protected static readonly EVENT_COMPONENT_CLICK_OUTSIDE = 'EVENT_COMPONENT_CLICK_OUTSIDE';
/**
* Listener's pool for trigger events
* @type {Map<string, EventListenerPool>}
*/
private readonly eventListenerPools: Map<string, EventListenerPool>;
/**
* Component theme
* @property {Theme}
*/
protected readonly theme: Theme;
/**
* Generic id if the property's id is missing
* @type {string}
*/
private genericId: string = ComponentUtilities.getNewGenericMyComponentId();
/**
* Root node in the DOM
* @type {Element | null | Text}
*/
protected rootNode: Element | null | Text;
/**
* @constructor
* @param props Properties for create the component
*/
constructor(props: P) {
super(props);
this.rootNode = null;
this.state = {} as unknown as S;
this.eventListenerPools = new Map<string, EventListenerPool>();
this.initComponentListeners();
this.theme = this.createTheme();
}
/**
* Get component's ID
* @returns ID property or the generic ID
*/
get id(): string {
let id = this.props.id;
if (id === undefined) {
id = this.genericId;
}
return id as string;
}
/**
* Get style for the component
* @returns component style
*/
protected get style(): React.CSSProperties | undefined {
return this.props.style;
}
/**
* Get disabled property
* @returns true if the component is disabled
*/
get isDisabled(): boolean | undefined {
return this.props.disabled;
}
/**
* Get component's class name
* @returns Class name
*/
protected get className(): string | undefined {
return this.props.className;
}
/**
* Get component's children
* @returns children
*/
protected get children(): React.ReactNode | undefined {
return this.props.children;
}
/**
* Create a new pool listner for specific event type
* @param eventType Event type
*/
protected createEventListenerPool(eventType: string): void {
this.eventListenerPools.set(eventType, new EventListenerPool());
}
/**
* Get the listener pool which is associated with the type of event
* @param eventType Event type
* @returns listener pool or undefined if the pool isn't created
*/
private getEventListenerPool(
eventType: string
): EventListenerPool | undefined {
return this.eventListenerPools.get(eventType);
}
/**
* Add a new listener in a listener pool
* @param eventType Event type
* @param listener Listener to add
*/
protected addListenerInListenerPool(eventType: string | Array<string>,
// eslint-disable-next-line @typescript-eslint/ban-types
listener?: Function): void {
if (listener) {
const pools = new Array<EventListenerPool>();
if (eventType instanceof Array) {
eventType.forEach((event) => {
const pool = this.getEventListenerPool(event);
if (pool) {
pools.push(pool);
}
});
} else {
const listenerPool = this.getEventListenerPool(eventType);
if (listenerPool) {
pools.push(listenerPool);
}
}
pools.forEach((pool) => pool.add(listener));
}
}
/**
* Basic method for fire an event
* @param eventType Event type key
* @param event Event to fire
*/
protected fireBasicEventType(eventType: string, event?: unknown): void {
const listenerPool = this.getEventListenerPool(eventType);
if (listenerPool) {
listenerPool.fireEvent(event);
}
}
/**
* Initialize component listeners
*/
protected initComponentListeners(): void {
this.createEventListenerPool(MyComponent.EVENT_COMPONENT_DID_MOUNT);
this.addComponentDidMountListener(this.props.onComponentDidMount);
this.createEventListenerPool(MyComponent.EVENT_COMPONENT_WILL_UNMOUNT);
this.addComponentWillUnmountListener(this.props.onComponentWillUnmount);
this.createEventListenerPool(MyComponent.EVENT_COMPONENT_CLICK_OUTSIDE);
this.addClickOutsideListener(this.props.onClickOutside);
}
/**
* Add a listener when the component did mount
* @param listener Listener
*/
private addComponentDidMountListener(listener?: (() => void)): void {
this.addListenerInListenerPool(MyComponent.EVENT_COMPONENT_DID_MOUNT, listener);
}
/**
* Function called when component did mount.
* Add a default listener on mousedown event for manage the click outside the component
*/
componentDidMount(): void {
this.rootNode = findDOMNode(this);
document.addEventListener('mousedown', this.handleClickOutside.bind(this));
this.fireBasicEventType(MyComponent.EVENT_COMPONENT_DID_MOUNT);
}
/**
* Add a listener when the component will unmount
* @param listener Listener
*/
private addComponentWillUnmountListener(listener?: (() => void)): void {
this.addListenerInListenerPool(MyComponent.EVENT_COMPONENT_WILL_UNMOUNT, listener);
}
/**
* Add a listener when click outside the component
* @param listener Listener
*/
private addClickOutsideListener(listener?: ((event: MouseEvent) => void)): void {
this.addListenerInListenerPool(MyComponent.EVENT_COMPONENT_CLICK_OUTSIDE, listener);
}
/**
* Function called when the component will unmount
*/
componentWillUnmount(): void {
document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
this.fireBasicEventType(MyComponent.EVENT_COMPONENT_WILL_UNMOUNT);
}
/**
* Create the theme for the component
* @returns Theme for the component
*/
protected createTheme(): Theme {
return createMuiTheme(this.defaultComponentTheme());
}
/**
* Get the default theme for the component
* @returns Default theme
*/
private defaultComponentTheme(): MyTheme {
return defaultPalette;
}
/**
* The render for the React component. Wrap the compoent's render.
* @returns The element that will be used in the DOM
*/
render(): JSX.Element {
let element: JSX.Element;
element = this.renderComponent();
element = this.wrapElement(element);
return element;
}
/**
* The method for render compnent. Return only the component's render
* @returns Element will be used in the DOM for the component
*/
protected abstract renderComponent(): JSX.Element;
/**
* Wrap the element with a Box component
* @param oriElement Element to wrap
* @returns Wrapped element
*/
protected wrapElement(oriElement: JSX.Element): JSX.Element {
let element = oriElement;
const { boxProps } = this.props;
element = this.wrapWithBoxProps(element, boxProps);
return element;
}
protected wrapWithBoxProps(oriElement: JSX.Element, boxProps?: BoxProps): JSX.Element {
let element = oriElement;
if (boxProps) {
element = (
<Box {...boxProps}>
{element}
</Box>
);
}
return element;
}
/**
* Handle the event when click outside the element.
* Fire event only if the mousedown event is outside the component
* @param event The event mousedown
*/
protected handleClickOutside(event: MouseEvent): void {
if (!this.childOf(event.target as Element | null)) {
this.fireBasicEventType(MyComponent.EVENT_COMPONENT_CLICK_OUTSIDE, event);
}
}
/**
* Check if the element is in the component (in the DOM)
* @param node Node to check
* @returns true if the element is in the component DOM
*/
protected childOf(node: (Node & ParentNode) | null) {
let child = node;
let check = false;
while (child !== null) {
if (child === this.rootNode) {
check = true;
break;
}
child = child.parentNode;
}
return check;
}
}
然后我有一个显示警报的基本组件。
import { Alert, AlertTitle } from '@material-ui/lab';
import React from 'react';
import { MyComponent } from '../MyComponent/MyComponent';
import { MyAlertProperties, MyAlertState } from './MyAlert.types';
/**
* An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
* @extends MyComponent
*/
export class MyAlert extends MyComponent<MyAlertProperties, MyAlertState> {
static readonly VERSION: number = 100;
/**
* @override
*/
protected renderComponent(): JSX.Element {
const { elevation, severity, variant } = this.props;
const element = (
<Alert
elevation={elevation}
variant={variant}
severity={severity}
>
{this.getRenderTitle()}
{this.children}
</Alert>
);
return element;
}
/**
* Render the alert's title
* @returns Alert's title element
*/
private getRenderTitle(): JSX.Element {
const { title } = this.props;
let element = undefined as unknown as JSX.Element;
if (title) {
element = (<AlertTitle>{title}</AlertTitle>);
}
return element;
}
}
我用 rollupjs 构建了我的库,它通过故事书运行得很好。
然后我想将我的库与 React 应用程序中的 npm 依赖项相关联,我之前通过“create-react-app”使用打字稿模板创建了该应用程序。在 App.tsx 中,我调用我的组件并收到以下错误消息“无效挂钩调用”,而我没有在我的组件中使用任何组件。
import React from 'react';
import { MyAlert } from 'myreactlib/build';
import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<MyAlert>Alert</MyAlert>
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
serviceWorker.unregister();
如果我使用简单的 div,我的 App.tsx 工作正常。
我不明白我的问题出在哪里。我不使用钩子。我知道它在基于 class 的组件中是被禁止的。
有关信息,我的应用程序上的反应版本似乎不错:
- react@16.13.1
- 反应-dom@16.13.1
如果有人有想法?
谢谢
您的应用中可能有重复的 React 实例,这可能会生成此误导性错误消息。参见 https://github.com/facebook/react/issues/13991
确保 React 是一个对等依赖项,并且您没有将其捆绑在您的库中,并且您没有通过 npm link
.
使用您的库
我对 React 有一些疑问。我想创建一个 class 形式的组件库(我来自 Java 世界,我发现 OOP 更具可读性)。
为此,我创建了一个基础 class,我的所有组件都将从该基础继承。
import { Box, createMuiTheme, Theme } from '@material-ui/core';
import React from 'react';
import { findDOMNode } from 'react-dom';
import { MyTheme, defaultPalette } from '../MyTheme/MyTheme';
import { BoxProps } from './BoxProps';
import { MyComponentProperties, MyComponentState } from './MyComponent.types';
import { ComponentUtilities } from './ComponentUtilities';
import { EventListenerPool } from './EventListenerPool';
/**
* Default class for create a Component with this library
* @abstract
*/
export abstract class
MyComponent<P extends MyComponentProperties, S extends MyComponentState>
extends React.Component<P, S> {
static readonly VERSION: number = 100;
/**
* Constant for the event : componentDidMount
* @constant EVENT_COMPONENT_DID_MOUNT
*/
protected static readonly EVENT_COMPONENT_DID_MOUNT = 'EVENT_COMPONENT_DID_MOUNT';
/**
* Constant for the event : componentWillUnmount
* @constant EVENT_COMPONENT_WILL_UNMOUNT
*/
protected static readonly EVENT_COMPONENT_WILL_UNMOUNT = 'EVENT_COMPONENT_WILL_UNMOUNT';
/**
* Constant for the event : clickOutside
* @constant EVENT_COMPONENT_CLICK_OUTSIDE
*/
protected static readonly EVENT_COMPONENT_CLICK_OUTSIDE = 'EVENT_COMPONENT_CLICK_OUTSIDE';
/**
* Listener's pool for trigger events
* @type {Map<string, EventListenerPool>}
*/
private readonly eventListenerPools: Map<string, EventListenerPool>;
/**
* Component theme
* @property {Theme}
*/
protected readonly theme: Theme;
/**
* Generic id if the property's id is missing
* @type {string}
*/
private genericId: string = ComponentUtilities.getNewGenericMyComponentId();
/**
* Root node in the DOM
* @type {Element | null | Text}
*/
protected rootNode: Element | null | Text;
/**
* @constructor
* @param props Properties for create the component
*/
constructor(props: P) {
super(props);
this.rootNode = null;
this.state = {} as unknown as S;
this.eventListenerPools = new Map<string, EventListenerPool>();
this.initComponentListeners();
this.theme = this.createTheme();
}
/**
* Get component's ID
* @returns ID property or the generic ID
*/
get id(): string {
let id = this.props.id;
if (id === undefined) {
id = this.genericId;
}
return id as string;
}
/**
* Get style for the component
* @returns component style
*/
protected get style(): React.CSSProperties | undefined {
return this.props.style;
}
/**
* Get disabled property
* @returns true if the component is disabled
*/
get isDisabled(): boolean | undefined {
return this.props.disabled;
}
/**
* Get component's class name
* @returns Class name
*/
protected get className(): string | undefined {
return this.props.className;
}
/**
* Get component's children
* @returns children
*/
protected get children(): React.ReactNode | undefined {
return this.props.children;
}
/**
* Create a new pool listner for specific event type
* @param eventType Event type
*/
protected createEventListenerPool(eventType: string): void {
this.eventListenerPools.set(eventType, new EventListenerPool());
}
/**
* Get the listener pool which is associated with the type of event
* @param eventType Event type
* @returns listener pool or undefined if the pool isn't created
*/
private getEventListenerPool(
eventType: string
): EventListenerPool | undefined {
return this.eventListenerPools.get(eventType);
}
/**
* Add a new listener in a listener pool
* @param eventType Event type
* @param listener Listener to add
*/
protected addListenerInListenerPool(eventType: string | Array<string>,
// eslint-disable-next-line @typescript-eslint/ban-types
listener?: Function): void {
if (listener) {
const pools = new Array<EventListenerPool>();
if (eventType instanceof Array) {
eventType.forEach((event) => {
const pool = this.getEventListenerPool(event);
if (pool) {
pools.push(pool);
}
});
} else {
const listenerPool = this.getEventListenerPool(eventType);
if (listenerPool) {
pools.push(listenerPool);
}
}
pools.forEach((pool) => pool.add(listener));
}
}
/**
* Basic method for fire an event
* @param eventType Event type key
* @param event Event to fire
*/
protected fireBasicEventType(eventType: string, event?: unknown): void {
const listenerPool = this.getEventListenerPool(eventType);
if (listenerPool) {
listenerPool.fireEvent(event);
}
}
/**
* Initialize component listeners
*/
protected initComponentListeners(): void {
this.createEventListenerPool(MyComponent.EVENT_COMPONENT_DID_MOUNT);
this.addComponentDidMountListener(this.props.onComponentDidMount);
this.createEventListenerPool(MyComponent.EVENT_COMPONENT_WILL_UNMOUNT);
this.addComponentWillUnmountListener(this.props.onComponentWillUnmount);
this.createEventListenerPool(MyComponent.EVENT_COMPONENT_CLICK_OUTSIDE);
this.addClickOutsideListener(this.props.onClickOutside);
}
/**
* Add a listener when the component did mount
* @param listener Listener
*/
private addComponentDidMountListener(listener?: (() => void)): void {
this.addListenerInListenerPool(MyComponent.EVENT_COMPONENT_DID_MOUNT, listener);
}
/**
* Function called when component did mount.
* Add a default listener on mousedown event for manage the click outside the component
*/
componentDidMount(): void {
this.rootNode = findDOMNode(this);
document.addEventListener('mousedown', this.handleClickOutside.bind(this));
this.fireBasicEventType(MyComponent.EVENT_COMPONENT_DID_MOUNT);
}
/**
* Add a listener when the component will unmount
* @param listener Listener
*/
private addComponentWillUnmountListener(listener?: (() => void)): void {
this.addListenerInListenerPool(MyComponent.EVENT_COMPONENT_WILL_UNMOUNT, listener);
}
/**
* Add a listener when click outside the component
* @param listener Listener
*/
private addClickOutsideListener(listener?: ((event: MouseEvent) => void)): void {
this.addListenerInListenerPool(MyComponent.EVENT_COMPONENT_CLICK_OUTSIDE, listener);
}
/**
* Function called when the component will unmount
*/
componentWillUnmount(): void {
document.removeEventListener('mousedown', this.handleClickOutside.bind(this));
this.fireBasicEventType(MyComponent.EVENT_COMPONENT_WILL_UNMOUNT);
}
/**
* Create the theme for the component
* @returns Theme for the component
*/
protected createTheme(): Theme {
return createMuiTheme(this.defaultComponentTheme());
}
/**
* Get the default theme for the component
* @returns Default theme
*/
private defaultComponentTheme(): MyTheme {
return defaultPalette;
}
/**
* The render for the React component. Wrap the compoent's render.
* @returns The element that will be used in the DOM
*/
render(): JSX.Element {
let element: JSX.Element;
element = this.renderComponent();
element = this.wrapElement(element);
return element;
}
/**
* The method for render compnent. Return only the component's render
* @returns Element will be used in the DOM for the component
*/
protected abstract renderComponent(): JSX.Element;
/**
* Wrap the element with a Box component
* @param oriElement Element to wrap
* @returns Wrapped element
*/
protected wrapElement(oriElement: JSX.Element): JSX.Element {
let element = oriElement;
const { boxProps } = this.props;
element = this.wrapWithBoxProps(element, boxProps);
return element;
}
protected wrapWithBoxProps(oriElement: JSX.Element, boxProps?: BoxProps): JSX.Element {
let element = oriElement;
if (boxProps) {
element = (
<Box {...boxProps}>
{element}
</Box>
);
}
return element;
}
/**
* Handle the event when click outside the element.
* Fire event only if the mousedown event is outside the component
* @param event The event mousedown
*/
protected handleClickOutside(event: MouseEvent): void {
if (!this.childOf(event.target as Element | null)) {
this.fireBasicEventType(MyComponent.EVENT_COMPONENT_CLICK_OUTSIDE, event);
}
}
/**
* Check if the element is in the component (in the DOM)
* @param node Node to check
* @returns true if the element is in the component DOM
*/
protected childOf(node: (Node & ParentNode) | null) {
let child = node;
let check = false;
while (child !== null) {
if (child === this.rootNode) {
check = true;
break;
}
child = child.parentNode;
}
return check;
}
}
然后我有一个显示警报的基本组件。
import { Alert, AlertTitle } from '@material-ui/lab';
import React from 'react';
import { MyComponent } from '../MyComponent/MyComponent';
import { MyAlertProperties, MyAlertState } from './MyAlert.types';
/**
* An alert displays a short, important message in a way that attracts the user's attention without interrupting the user's task.
* @extends MyComponent
*/
export class MyAlert extends MyComponent<MyAlertProperties, MyAlertState> {
static readonly VERSION: number = 100;
/**
* @override
*/
protected renderComponent(): JSX.Element {
const { elevation, severity, variant } = this.props;
const element = (
<Alert
elevation={elevation}
variant={variant}
severity={severity}
>
{this.getRenderTitle()}
{this.children}
</Alert>
);
return element;
}
/**
* Render the alert's title
* @returns Alert's title element
*/
private getRenderTitle(): JSX.Element {
const { title } = this.props;
let element = undefined as unknown as JSX.Element;
if (title) {
element = (<AlertTitle>{title}</AlertTitle>);
}
return element;
}
}
我用 rollupjs 构建了我的库,它通过故事书运行得很好。
然后我想将我的库与 React 应用程序中的 npm 依赖项相关联,我之前通过“create-react-app”使用打字稿模板创建了该应用程序。在 App.tsx 中,我调用我的组件并收到以下错误消息“无效挂钩调用”,而我没有在我的组件中使用任何组件。
import React from 'react';
import { MyAlert } from 'myreactlib/build';
import ReactDOM from 'react-dom';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<MyAlert>Alert</MyAlert>
</React.StrictMode>,
document.getElementById('root')
);
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
serviceWorker.unregister();
如果我使用简单的 div,我的 App.tsx 工作正常。
我不明白我的问题出在哪里。我不使用钩子。我知道它在基于 class 的组件中是被禁止的。 有关信息,我的应用程序上的反应版本似乎不错:
- react@16.13.1
- 反应-dom@16.13.1
如果有人有想法? 谢谢
您的应用中可能有重复的 React 实例,这可能会生成此误导性错误消息。参见 https://github.com/facebook/react/issues/13991
确保 React 是一个对等依赖项,并且您没有将其捆绑在您的库中,并且您没有通过 npm link
.