Skip to content

Commit 13b05a2

Browse files
committed
Replace EuiObserver abstract class with useObserver hook (#9458)
Convert the abstract `EuiObserver` base class to a shared `useObserver` custom hook. Migrate both `EuiResizeObserver` and `EuiMutationObserver` from class components extending `EuiObserver` to function components that use the new hook. - Replace `EuiObserver` class with `useObserver` hook in `observer.ts` - Convert `EuiResizeObserver` to a function component using `useObserver` - Convert `EuiMutationObserver` to a function component using `useObserver` - All existing tests pass - No public API changes
1 parent b2a2760 commit 13b05a2

File tree

4 files changed

+98
-85
lines changed

4 files changed

+98
-85
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
- Replaced `EuiObserver` abstract base class with a `useObserver` hook
2+
- Converted `EuiResizeObserver` from a class component to a function component
3+
- Converted `EuiMutationObserver` from a class component to a function component

packages/eui/src/components/observer/mutation_observer/mutation_observer.tsx

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* Side Public License, v 1.
77
*/
88

9-
import { ReactNode, useEffect } from 'react';
9+
import React, { ReactNode, useCallback, useEffect, useRef } from 'react';
1010

11-
import { EuiObserver } from '../observer';
11+
import { useObserver } from '../observer';
1212

1313
export interface EuiMutationObserverProps {
1414
/**
@@ -19,24 +19,35 @@ export interface EuiMutationObserverProps {
1919
observerOptions?: MutationObserverInit;
2020
}
2121

22-
export class EuiMutationObserver extends EuiObserver<EuiMutationObserverProps> {
23-
name = 'EuiMutationObserver';
22+
export const EuiMutationObserver: React.FunctionComponent<
23+
EuiMutationObserverProps
24+
> = ({ children, onMutation, observerOptions }) => {
25+
// Store onMutation and observerOptions in refs so the observer callback
26+
// and setup always use the latest prop values without needing to
27+
// re-subscribe (which would cause the ref callback to cycle)
28+
const onMutationRef = useRef<MutationCallback>(onMutation);
29+
onMutationRef.current = onMutation;
2430

25-
// the `onMutation` prop may change while the observer is bound, abstracting
26-
// it out into a separate function means the current `onMutation` value is used
27-
onMutation: MutationCallback = (records, observer) => {
28-
this.props.onMutation(records, observer);
29-
};
31+
const observerOptionsRef = useRef(observerOptions);
32+
observerOptionsRef.current = observerOptions;
3033

31-
beginObserve = () => {
32-
const childNode = this.childNode!;
33-
this.observer = makeMutationObserver(
34-
childNode,
35-
this.props.observerOptions,
36-
this.onMutation
37-
);
38-
};
39-
}
34+
const mutationCallback: MutationCallback = useCallback(
35+
(records, observer) => {
36+
onMutationRef.current(records, observer);
37+
},
38+
[]
39+
);
40+
41+
const beginObserve = useCallback(
42+
(node: Element) =>
43+
makeMutationObserver(node, observerOptionsRef.current, mutationCallback),
44+
[mutationCallback]
45+
);
46+
47+
const updateChildNode = useObserver(beginObserve);
48+
49+
return children(updateChildNode) as React.ReactElement;
50+
};
4051

4152
const makeMutationObserver = (
4253
node: Element,

packages/eui/src/components/observer/observer.ts

Lines changed: 37 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,59 +6,53 @@
66
* Side Public License, v 1.
77
*/
88

9-
import { Component, ReactNode } from 'react';
10-
11-
interface BaseProps {
12-
/**
13-
* ReactNode to render as this component's content
14-
*/
15-
children: (ref: any) => ReactNode;
16-
}
9+
import { useCallback, useEffect, useRef } from 'react';
1710

1811
export interface Observer {
1912
disconnect: () => void;
2013
observe: (element: Element, options?: { [key: string]: any }) => void;
2114
}
2215

23-
export class EuiObserver<Props extends BaseProps> extends Component<Props> {
24-
protected name: string = 'EuiObserver';
25-
protected childNode: null | Element = null;
26-
protected observer: null | Observer = null;
27-
28-
componentDidMount() {
29-
if (this.childNode == null) {
30-
throw new Error(`${this.name} did not receive a ref`);
31-
}
32-
}
33-
34-
componentWillUnmount() {
35-
if (this.observer != null) {
36-
this.observer.disconnect();
37-
}
38-
}
39-
40-
updateChildNode = (ref: Element) => {
41-
if (this.childNode === ref) return; // node hasn't changed
16+
/**
17+
* A shared custom hook that provides a pattern for observing DOM nodes
18+
* via browser observer APIs. Used by `EuiResizeObserver`.
19+
*
20+
* @param beginObserve - A callback that receives the target DOM element and should
21+
* create and return the observer instance (e.g., `ResizeObserver`).
22+
*/
23+
export const useObserver = (
24+
beginObserve: (node: Element) => Observer | undefined
25+
) => {
26+
const childNodeRef = useRef<Element | null>(null);
27+
const observerRef = useRef<Observer | null>(null);
28+
29+
// Store beginObserve in a ref so the ref callback doesn't cycle
30+
const beginObserveRef = useRef(beginObserve);
31+
beginObserveRef.current = beginObserve;
32+
33+
// Clean up observer on unmount
34+
useEffect(() => {
35+
return () => {
36+
observerRef.current?.disconnect();
37+
};
38+
}, []);
39+
40+
const updateChildNode = useCallback((ref: Element | null) => {
41+
if (childNodeRef.current === ref) return; // node hasn't changed
4242

4343
// if there's an existing observer disconnect it
44-
if (this.observer != null) {
45-
this.observer.disconnect();
46-
this.observer = null;
44+
if (observerRef.current != null) {
45+
observerRef.current.disconnect();
46+
observerRef.current = null;
4747
}
4848

49-
this.childNode = ref;
49+
childNodeRef.current = ref;
5050

51-
if (this.childNode != null) {
52-
this.beginObserve();
51+
if (childNodeRef.current != null) {
52+
observerRef.current =
53+
beginObserveRef.current(childNodeRef.current) ?? null;
5354
}
54-
};
55+
}, []);
5556

56-
beginObserve: () => void = () => {
57-
throw new Error('EuiObserver has no default observation method');
58-
};
59-
60-
render() {
61-
const props: BaseProps = this.props;
62-
return props.children(this.updateChildNode);
63-
}
64-
}
57+
return updateChildNode;
58+
};

packages/eui/src/components/observer/resize_observer/resize_observer.tsx

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,14 @@
66
* Side Public License, v 1.
77
*/
88

9-
import { ReactNode, useCallback, useEffect, useRef, useState } from 'react';
10-
import { EuiObserver } from '../observer';
9+
import React, {
10+
ReactNode,
11+
useCallback,
12+
useEffect,
13+
useRef,
14+
useState,
15+
} from 'react';
16+
import { useObserver } from '../observer';
1117

1218
export interface EuiResizeObserverProps {
1319
/**
@@ -20,36 +26,35 @@ export interface EuiResizeObserverProps {
2026
export const hasResizeObserver =
2127
typeof window !== 'undefined' && typeof window.ResizeObserver !== 'undefined';
2228

23-
export class EuiResizeObserver extends EuiObserver<EuiResizeObserverProps> {
24-
name = 'EuiResizeObserver';
29+
export const EuiResizeObserver: React.FunctionComponent<
30+
EuiResizeObserverProps
31+
> = ({ children, onResize }) => {
32+
const onResizeRef = useRef(onResize);
33+
onResizeRef.current = onResize;
2534

26-
state = {
27-
height: 0,
28-
width: 0,
29-
};
35+
const sizeRef = useRef({ height: 0, width: 0 });
3036

31-
onResize: ResizeObserverCallback = ([entry]) => {
37+
const resizeCallback: ResizeObserverCallback = useCallback(([entry]) => {
3238
const { inlineSize: width, blockSize: height } = entry.borderBoxSize[0];
3339

3440
// Check for actual resize event
35-
if (this.state.height === height && this.state.width === width) {
41+
if (sizeRef.current.height === height && sizeRef.current.width === width) {
3642
return;
3743
}
3844

39-
this.props.onResize({
40-
height,
41-
width,
42-
});
43-
this.setState({ height, width });
44-
};
45-
46-
beginObserve = () => {
47-
// The superclass checks that childNode is not null before invoking
48-
// beginObserve()
49-
const childNode = this.childNode!;
50-
this.observer = makeResizeObserver(childNode, this.onResize)!;
51-
};
52-
}
45+
sizeRef.current = { height, width };
46+
onResizeRef.current({ height, width });
47+
}, []);
48+
49+
const beginObserve = useCallback(
50+
(node: Element) => makeResizeObserver(node, resizeCallback),
51+
[resizeCallback]
52+
);
53+
54+
const updateChildNode = useObserver(beginObserve);
55+
56+
return children(updateChildNode) as React.ReactElement;
57+
};
5358

5459
const makeResizeObserver = (
5560
node: Element,

0 commit comments

Comments
 (0)