diff --git a/packages/core/client/src/application/schema-initializer/withInitializer.tsx b/packages/core/client/src/application/schema-initializer/withInitializer.tsx index 6fff914fca..1f2bce5c99 100644 --- a/packages/core/client/src/application/schema-initializer/withInitializer.tsx +++ b/packages/core/client/src/application/schema-initializer/withInitializer.tsx @@ -14,10 +14,11 @@ import React, { ComponentType, useCallback, useMemo, useState } from 'react'; import { css } from '@emotion/css'; import { useNiceDropdownMaxHeight } from '../../common/useNiceDropdownHeight'; import { useFlag } from '../../flag-provider'; -import { useDesignable } from '../../schema-component'; +import { ErrorFallback, useDesignable } from '../../schema-component'; import { useSchemaInitializerStyles } from './components/style'; import { SchemaInitializerContext } from './context'; import { SchemaInitializerOptions } from './types'; +import { ErrorBoundary } from 'react-error-boundary'; const defaultWrap = (s: ISchema) => s; @@ -87,53 +88,55 @@ export function withInitializer(C: ComponentType) { } return ( - - {popover === false ? ( - React.createElement(C, cProps) - ) : ( - - console.error(err)}> + + {popover === false ? ( + React.createElement(C, cProps) + ) : ( + - {children} - - , - )} - > - {React.createElement(C, cProps)} - - )} - + + {children} + + , + )} + > + {React.createElement(C, cProps)} + + )} + + ); }, { displayName: `WithInitializer(${C.displayName || C.name})` }, diff --git a/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx b/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx index f80a135dcb..61d4eabd9a 100644 --- a/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx +++ b/packages/core/client/src/application/schema-settings/components/SchemaSettingsChildren.tsx @@ -10,7 +10,7 @@ import React, { FC, memo, useEffect, useMemo, useRef } from 'react'; import { useFieldComponentName } from '../../../common/useFieldComponentName'; -import { useFindComponent } from '../../../schema-component'; +import { ErrorFallback, useFindComponent } from '../../../schema-component'; import { SchemaSettingsActionModalItem, SchemaSettingsCascaderItem, @@ -27,6 +27,7 @@ import { } from '../../../schema-settings/SchemaSettings'; import { SchemaSettingItemContext } from '../context/SchemaSettingItemContext'; import { SchemaSettingsItemType } from '../types'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; export interface SchemaSettingsChildrenProps { children: SchemaSettingsItemType[]; @@ -46,6 +47,19 @@ const typeComponentMap = { modal: SchemaSettingsModalItem, }; +const SchemaSettingsChildErrorFallback: FC< + FallbackProps & { + title: string; + } +> = (props) => { + const { title, ...fallbackProps } = props; + return ( + + + + ); +}; + export const SchemaSettingsChildren: FC = (props) => { const { children } = props; const { visible } = useSchemaSettings(); @@ -70,7 +84,15 @@ export const SchemaSettingsChildren: FC = (props) = // 两次渲染之间 props 可能发生变化,就可能报 hooks 调用顺序的错误。所以这里使用 fieldComponentName 和 item.name 拼成 // 一个不会重复的 key,保证每次渲染都是新的组件。 const key = `${fieldComponentName ? fieldComponentName + '-' : ''}${item.name}`; - return ; + return ( + } + onError={(err) => console.log(err)} + > + + + ); })} ); diff --git a/packages/core/client/src/application/schema-toolbar/hooks/index.tsx b/packages/core/client/src/application/schema-toolbar/hooks/index.tsx index ddb4d3d7df..8d0e645ee4 100644 --- a/packages/core/client/src/application/schema-toolbar/hooks/index.tsx +++ b/packages/core/client/src/application/schema-toolbar/hooks/index.tsx @@ -9,14 +9,29 @@ import { ISchema } from '@formily/json-schema'; import React, { useMemo } from 'react'; -import { useComponent, useDesignable } from '../../../schema-component'; +import { ErrorFallback, useComponent, useDesignable } from '../../../schema-component'; import { SchemaToolbar, SchemaToolbarProps } from '../../../schema-settings/GeneralSchemaDesigner'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; + +const SchemaToolbarErrorFallback: React.FC = (props) => { + const { designable } = useDesignable(); + + if (!designable) { + return null; + } + + return ( + + + + ); +}; export const useSchemaToolbarRender = (fieldSchema: ISchema) => { const { designable } = useDesignable(); const toolbar = useMemo(() => { - if (fieldSchema['x-toolbar'] || fieldSchema['x-designer']) { - return fieldSchema['x-toolbar'] || fieldSchema['x-designer']; + if (fieldSchema['x-designer'] || fieldSchema['x-toolbar']) { + return fieldSchema['x-designer'] || fieldSchema['x-toolbar']; } if (fieldSchema['x-settings']) { @@ -30,7 +45,11 @@ export const useSchemaToolbarRender = (fieldSchema: ISchema) => { if (!designable || !C) { return null; } - return ; + return ( + console.error(err)}> + + + ); }, exists: !!C, }; diff --git a/packages/core/client/src/data-source/collection-field/CollectionField.tsx b/packages/core/client/src/data-source/collection-field/CollectionField.tsx index ee8193d86e..c3485282e6 100644 --- a/packages/core/client/src/data-source/collection-field/CollectionField.tsx +++ b/packages/core/client/src/data-source/collection-field/CollectionField.tsx @@ -14,9 +14,10 @@ import { concat } from 'lodash'; import React, { useCallback, useEffect, useMemo } from 'react'; import { useFormBlockContext } from '../../block-provider/FormBlockProvider'; import { useDynamicComponentProps } from '../../hoc/withDynamicSchemaProps'; -import { useCompile, useComponent } from '../../schema-component'; +import { ErrorFallback, useCompile, useComponent } from '../../schema-component'; import { useIsAllowToSetDefaultValue } from '../../schema-settings/hooks/useIsAllowToSetDefaultValue'; import { CollectionFieldProvider, useCollectionField } from './CollectionFieldProvider'; +import { ErrorBoundary } from 'react-error-boundary'; type Props = { component: any; @@ -96,9 +97,11 @@ export const CollectionFieldInternalField: React.FC = (props: Props) => { export const CollectionField = connect((props) => { const fieldSchema = useFieldSchema(); return ( - - - + console.log(err)}> + + + + ); }); diff --git a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx index c420af24d2..1cf8c900dd 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Drawer.tsx @@ -11,18 +11,29 @@ import { observer, RecursionField, useField, useFieldSchema } from '@formily/rea import { Drawer } from 'antd'; import classNames from 'classnames'; import React from 'react'; -import { OpenSize } from './types'; +import { ActionDrawerProps, OpenSize } from './types'; import { useStyles } from './Action.Drawer.style'; import { useActionContext } from './hooks'; import { useSetAriaLabelForDrawer } from './hooks/useSetAriaLabelForDrawer'; import { ComposedActionDrawer } from './types'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { ErrorFallback } from '../error-fallback'; + +const DrawerErrorFallback: React.FC = (props) => { + const { visible, setVisible } = useActionContext(); + return ( + setVisible(false, true)} width="50%"> + + + ); +}; const openSizeWidthMap = new Map([ ['small', '30%'], ['middle', '50%'], ['large', '70%'], ]); -export const ActionDrawer: ComposedActionDrawer = observer( +export const InternalActionDrawer: React.FC = observer( (props) => { const { footerNodeName = 'Action.Drawer.Footer', ...others } = props; const { visible, setVisible, openSize = 'middle', drawerProps, modalProps } = useActionContext(); @@ -83,6 +94,12 @@ export const ActionDrawer: ComposedActionDrawer = observer( { displayName: 'ActionDrawer' }, ); +export const ActionDrawer: ComposedActionDrawer = (props) => ( + console.log(err)}> + + +); + ActionDrawer.Footer = observer( () => { const field = useField(); diff --git a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx index 14f2738062..c4d4a2f086 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.Modal.tsx @@ -14,15 +14,26 @@ import classNames from 'classnames'; import React from 'react'; import { useToken } from '../../../style'; import { useSetAriaLabelForModal } from './hooks/useSetAriaLabelForModal'; -import { ComposedActionDrawer, OpenSize } from './types'; +import { ActionDrawerProps, ComposedActionDrawer, OpenSize } from './types'; import { useActionContext } from './hooks'; +import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; +import { ErrorFallback } from '../error-fallback'; + +const ModalErrorFallback: React.FC = (props) => { + const { visible, setVisible } = useActionContext(); + return ( + setVisible(false, true)} width="60%"> + + + ); +}; const openSizeWidthMap = new Map([ ['small', '40%'], ['middle', '60%'], ['large', '80%'], ]); -export const ActionModal: ComposedActionDrawer = observer( +export const InternalActionModal: React.FC> = observer( (props) => { const { footerNodeName = 'Action.Modal.Footer', width, ...others } = props; const { visible, setVisible, openSize = 'middle', modalProps } = useActionContext(); @@ -116,6 +127,12 @@ export const ActionModal: ComposedActionDrawer = observer( { displayName: 'ActionModal' }, ); +export const ActionModal: ComposedActionDrawer = (props) => ( + console.log(err)}> + + +); + ActionModal.Footer = observer( () => { const field = useField(); diff --git a/packages/core/client/src/schema-component/antd/action/Action.tsx b/packages/core/client/src/schema-component/antd/action/Action.tsx index c6c0127834..fb9d23d32b 100644 --- a/packages/core/client/src/schema-component/antd/action/Action.tsx +++ b/packages/core/client/src/schema-component/antd/action/Action.tsx @@ -14,7 +14,7 @@ import classnames from 'classnames'; import { default as lodash } from 'lodash'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { StablePopover, useActionContext } from '../..'; +import { ErrorFallback, StablePopover, useActionContext } from '../..'; import { useDesignable } from '../../'; import { useACLActionParamsContext } from '../../../acl'; import { @@ -44,6 +44,9 @@ import { useA } from './hooks'; import { useGetAriaLabelOfAction } from './hooks/useGetAriaLabelOfAction'; import { ActionProps, ComposedAction } from './types'; import { linkageAction, setInitialActionState } from './utils'; +import { ErrorBoundary } from 'react-error-boundary'; + +const handleError = (err) => console.log(err); export const Action: ComposedAction = withDynamicSchemaProps( observer((props: ActionProps) => { @@ -246,6 +249,11 @@ export const Action: ComposedAction = withDynamicSchemaProps( Action.Popover = observer( (props) => { const { button, visible, setVisible } = useActionContext(); + const content = ( + + {props.children} + + ); return ( { setVisible(visible); }} - content={props.children} + content={content} > {button} diff --git a/packages/core/client/src/schema-component/antd/action/types.ts b/packages/core/client/src/schema-component/antd/action/types.ts index a03db6377a..dbd84ada09 100644 --- a/packages/core/client/src/schema-component/antd/action/types.ts +++ b/packages/core/client/src/schema-component/antd/action/types.ts @@ -86,6 +86,8 @@ export type ComposedAction = React.FC & { [key: string]: any; }; -export type ComposedActionDrawer = React.FC & { +export type ActionDrawerProps = T & { footerNodeName?: string }; + +export type ComposedActionDrawer = React.FC> & { Footer?: React.FC; }; diff --git a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx index acc5c30549..5f88e50ecd 100644 --- a/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx +++ b/packages/core/client/src/schema-component/antd/block-item/BlockItem.tsx @@ -13,8 +13,11 @@ import React, { useMemo } from 'react'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { CustomCreateStylesUtils, createStyles } from '../../../style'; import { SortableItem } from '../../common'; -import { useDesigner, useProps } from '../../hooks'; +import { useProps } from '../../hooks'; import { useGetAriaLabelOfBlockItem } from './hooks/useGetAriaLabelOfBlockItem'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorFallback } from '../error-fallback'; +import { useSchemaToolbarRender } from '../../../application'; const useStyles = createStyles(({ css, token }: CustomCreateStylesUtils) => { return css` @@ -79,16 +82,18 @@ export const BlockItem: React.FC = withDynamicSchemaProps( const { className, children } = useProps(props); const { styles: blockItemCss } = useStyles(); - const Designer = useDesigner(); const fieldSchema = useFieldSchema(); + const { render } = useSchemaToolbarRender(fieldSchema); const { getAriaLabel } = useGetAriaLabelOfBlockItem(props.name); const label = useMemo(() => getAriaLabel(), [getAriaLabel]); return ( - - {children} + {render()} + console.log(err)}> + {children} + ); }, diff --git a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx index 7c8a906f20..0e0af62082 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx +++ b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallback.tsx @@ -11,10 +11,13 @@ import { Button, Result, Typography } from 'antd'; import React, { FC } from 'react'; import { FallbackProps, useErrorBoundary } from 'react-error-boundary'; import { Trans, useTranslation } from 'react-i18next'; +import { ErrorFallbackModal } from './ErrorFallbackModal'; const { Paragraph, Text, Link } = Typography; -export const ErrorFallback: FC = ({ error }) => { +export const ErrorFallback: FC & { + Modal: FC; +} = ({ error }) => { const { resetBoundary } = useErrorBoundary(); const { t } = useTranslation(); @@ -52,3 +55,5 @@ export const ErrorFallback: FC = ({ error }) => { ); }; + +ErrorFallback.Modal = ErrorFallbackModal; diff --git a/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx new file mode 100644 index 0000000000..2c99836acc --- /dev/null +++ b/packages/core/client/src/schema-component/antd/error-fallback/ErrorFallbackModal.tsx @@ -0,0 +1,50 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import { Modal } from 'antd'; +import React, { FC } from 'react'; +import { FallbackProps } from 'react-error-boundary'; +import { ErrorFallback } from './ErrorFallback'; +import { Typography } from 'antd'; +const { Paragraph, Text } = Typography; + +export const ErrorFallbackModal: FC = (props) => { + const [open, setOpen] = React.useState(false); + const defaultChildren = ( + + + Error: {props.error.message} + + + ); + + return ( + <> +
setOpen(true)}>{props.children || defaultChildren}
+ setOpen(false)} width={'60%'}> + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/error-fallback/demos/demo1.tsx b/packages/core/client/src/schema-component/antd/error-fallback/__tests__/components/basic.tsx similarity index 84% rename from packages/core/client/src/schema-component/antd/error-fallback/demos/demo1.tsx rename to packages/core/client/src/schema-component/antd/error-fallback/__tests__/components/basic.tsx index 7297022a26..458d7fa36b 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/demos/demo1.tsx +++ b/packages/core/client/src/schema-component/antd/error-fallback/__tests__/components/basic.tsx @@ -1,8 +1,6 @@ - - import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { ErrorFallback } from '../ErrorFallback'; +import { ErrorFallback } from '../../ErrorFallback'; const App = () => { throw new Error('error message'); diff --git a/packages/core/client/src/schema-component/antd/error-fallback/__tests__/components/modal.tsx b/packages/core/client/src/schema-component/antd/error-fallback/__tests__/components/modal.tsx new file mode 100644 index 0000000000..34147acb51 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/error-fallback/__tests__/components/modal.tsx @@ -0,0 +1,24 @@ +/** + * This file is part of the NocoBase (R) project. + * Copyright (c) 2020-2024 NocoBase Co., Ltd. + * Authors: NocoBase Team. + * + * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License. + * For more information, please refer to: https://www.nocobase.com/agreement. + */ + +import React from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorFallback } from '../../ErrorFallback'; + +const App = () => { + throw new Error('error message'); +}; + +export default () => { + return ( + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/error-fallback/__tests__/error-fallback.test.tsx b/packages/core/client/src/schema-component/antd/error-fallback/__tests__/error-fallback.test.tsx index 60f4486952..fd6ef47b78 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/__tests__/error-fallback.test.tsx +++ b/packages/core/client/src/schema-component/antd/error-fallback/__tests__/error-fallback.test.tsx @@ -7,9 +7,10 @@ * For more information, please refer to: https://www.nocobase.com/agreement. */ -import { render, screen } from '@nocobase/test/client'; +import { render, screen, userEvent, waitFor } from '@nocobase/test/client'; import React from 'react'; -import App1 from '../demos/demo1'; +import App1 from './components/basic'; +import InlineApp from './components/modal'; describe('ErrorFallback', () => { it('should render correctly', () => { @@ -24,4 +25,19 @@ describe('ErrorFallback', () => { // 底部复制按钮 expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument(); }); + + it('should render inline correctly', async () => { + render(); + + expect(screen.getByText(/Error: error message/i)).toBeInTheDocument(); + expect(document.querySelector('.ant-typography-copy')).toBeInTheDocument(); + + await userEvent.hover(screen.getByText(/Error: error message/i)); + await waitFor(() => { + expect(screen.getByText(/render failed/i)).toBeInTheDocument(); + expect( + screen.getByText(/this is likely a nocobase internals bug\. please open an issue at/i), + ).toBeInTheDocument(); + }); + }); }); diff --git a/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/basic.tsx b/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/basic.tsx index ba731c8e7a..0b7181ad6b 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/basic.tsx +++ b/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/basic.tsx @@ -1,5 +1,3 @@ - - import React from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { ErrorFallback } from '@nocobase/client'; diff --git a/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/modal.tsx b/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/modal.tsx new file mode 100644 index 0000000000..c4f01b6dfe --- /dev/null +++ b/packages/core/client/src/schema-component/antd/error-fallback/demos/new-demos/modal.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { ErrorBoundary } from 'react-error-boundary'; +import { ErrorFallback } from '@nocobase/client'; +import { Button } from 'antd'; + +const App = () => { + const [showError, setShowError] = React.useState(false); + + if (showError) { + throw new Error('error message'); + } + + return ( + + ); +}; + +export default () => { + return ( + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/error-fallback/index.en-US.md b/packages/core/client/src/schema-component/antd/error-fallback/index.en-US.md index 3979c05a48..e1809572df 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/index.en-US.md +++ b/packages/core/client/src/schema-component/antd/error-fallback/index.en-US.md @@ -2,6 +2,12 @@ The component displayed when an error occurs during rendering. -其基于 [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 库。 +Based on the [react-error-boundary](https://github.com/bvaughn/react-error-boundary) library. + +## Basic + +## Modal + + diff --git a/packages/core/client/src/schema-component/antd/error-fallback/index.md b/packages/core/client/src/schema-component/antd/error-fallback/index.md index e0347d6210..fa9cbfafaf 100644 --- a/packages/core/client/src/schema-component/antd/error-fallback/index.md +++ b/packages/core/client/src/schema-component/antd/error-fallback/index.md @@ -6,4 +6,10 @@ 其基于 [react-error-boundary](https://github.com/bvaughn/react-error-boundary) 库。 +## Basic + + +## Modal + + diff --git a/packages/core/client/src/schema-component/hooks/useDesigner.ts b/packages/core/client/src/schema-component/hooks/useDesigner.ts index b0b88ebe33..f688a2e7f3 100644 --- a/packages/core/client/src/schema-component/hooks/useDesigner.ts +++ b/packages/core/client/src/schema-component/hooks/useDesigner.ts @@ -14,6 +14,10 @@ import { SchemaToolbar } from '../../schema-settings'; const DefaultSchemaToolbar = () => null; +/** + * @deprecated + * use `useSchemaToolbarRender` instead + */ export const useDesigner = () => { const { designable } = useDesignable(); const fieldSchema = useFieldSchema(); diff --git a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx index a4b6aeea3d..7f0fcef5a5 100644 --- a/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx +++ b/packages/core/client/src/schema-settings/GeneralSchemaDesigner.tsx @@ -200,7 +200,10 @@ const InternalSchemaToolbar: FC = (props) => { showBackground, showBorder = true, draggable = true, - } = { ...props, ...(fieldSchema['x-toolbar-props'] || {}) } as SchemaToolbarProps; + } = { + ...props, + ...(fieldSchema['x-toolbar-props'] || {}), + } as SchemaToolbarProps; const { designable } = useDesignable(); const compile = useCompile(); const { styles } = useStyles(); @@ -271,7 +274,14 @@ const InternalSchemaToolbar: FC = (props) => { useEffect(() => { const toolbarElement = toolbarRef.current; - const parentElement = toolbarElement?.parentElement; + let parentElement = toolbarElement?.parentElement; + while (parentElement && window.getComputedStyle(parentElement).height === '0px') { + parentElement = parentElement.parentElement; + } + if (!parentElement) { + return; + } + function show() { if (toolbarElement) { toolbarElement.style.display = 'block'; @@ -284,21 +294,17 @@ const InternalSchemaToolbar: FC = (props) => { } } - if (parentElement) { - const style = window.getComputedStyle(parentElement); - if (style.position === 'static') { - parentElement.style.position = 'relative'; - } - - parentElement.addEventListener('mouseenter', show); - parentElement.addEventListener('mouseleave', hide); + const style = window.getComputedStyle(parentElement); + if (style.position === 'static') { + parentElement.style.position = 'relative'; } + parentElement.addEventListener('mouseenter', show); + parentElement.addEventListener('mouseleave', hide); + return () => { - if (parentElement) { - parentElement.removeEventListener('mouseenter', show); - parentElement.removeEventListener('mouseleave', hide); - } + parentElement.removeEventListener('mouseenter', show); + parentElement.removeEventListener('mouseleave', hide); }; }, []);