Skip to content

Replace EuiObserver abstract class with useObserver hook#9511

Open
ragini-pandey wants to merge 4 commits intoelastic:mainfrom
ragini-pandey:fix/9458-observer-hook
Open

Replace EuiObserver abstract class with useObserver hook#9511
ragini-pandey wants to merge 4 commits intoelastic:mainfrom
ragini-pandey:fix/9458-observer-hook

Conversation

@ragini-pandey
Copy link
Contributor

Summary

  • What: Replaced the abstract EuiObserver base class with a shared useObserver custom hook. Converted both EuiResizeObserver and EuiMutationObserver from class components to function components.
  • Why: Closes [EuiObserver] Migrate abstract base class to a hook #9458 — Migrate abstract base class to a hook-based pattern as part of the class-to-hooks migration effort.
  • How:
    • observer.ts: Replaced the EuiObserver class with a useObserver hook that accepts a beginObserve callback (receives a DOM node, returns an observer instance). The hook manages the ref callback, observer lifecycle, and cleanup.
    • resize_observer.tsx: Converted EuiResizeObserver from a class extending EuiObserver to a function component using useObserver. Used refs instead of setState for tracking dimensions to avoid unnecessary re-renders.
    • mutation_observer.tsx: Converted EuiMutationObserver from a class extending EuiObserver to a function component using useObserver. The existing useMutationObserver hook and makeMutationObserver helper are unchanged.
    • The Observer interface and hasResizeObserver exports are preserved. No public API changes.

API Changes

component / parent prop / child change description
No public API changes

Screenshots

N/A — No visual changes. This is an internal refactor only.

Impact Assessment

  • 🔴 Breaking changes — No breaking changes. Public API is identical.
  • 💅 Visual changes — No visual changes.
  • 🧪 Test impact — No test changes needed. All existing tests pass.
  • 🔧 Hard to integrate — No integration impact. Drop-in replacement.

Impact level: 🟢 None

Release Readiness

  • Changelog: Added changelog entry
  • Documentation: No docs changes needed (internal refactor)
  • Figma: N/A
  • Migration guide: N/A (no breaking changes)
  • Adoption plan: N/A (internal refactor)

QA instructions for reviewer

  1. Verify EuiResizeObserver still works by checking components that use it (e.g., accordion, context menu panel, markdown editor, tooltip)
  2. Verify EuiMutationObserver still works by checking the popover component
  3. Run the observer tests: cd packages/eui && yarn test-unit --testPathPattern=observer
  4. Run TypeScript check: cd packages/eui && npx tsc --noEmit

Checklist before marking Ready for Review

  • Filled out all sections above
  • QA: Verified all observer tests pass, TypeScript compiles with no errors
  • Tests: Existing tests pass with no modifications needed
  • Changelog: Added changelog entry
  • Breaking changes: N/A

@ragini-pandey ragini-pandey requested a review from a team as a code owner March 16, 2026 16:47
@ragini-pandey ragini-pandey force-pushed the fix/9458-observer-hook branch from 13b05a2 to f7f685d Compare March 16, 2026 16:47
@github-actions
Copy link

👋 Since this is a community submitted pull request, a Buildkite build has not been started automatically. Would an Elastic organization member please verify the contents of this pull request and kick off a build manually?

@weronikaolejniczak
Copy link
Contributor

@ragini-pandey The 2 PRs you opened both migrate EuiMutationObserver. Could we close #9508 and focus on this one instead?

It would resolve:

We could link all of these in the PR description so they get automatically closed 😄

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the observer utilities in EUI away from a shared abstract class (EuiObserver) into a shared hook (useObserver) as part of the class-to-hooks migration effort (issue #9458), while keeping the EuiResizeObserver and EuiMutationObserver public component APIs intact.

Changes:

  • Replaced the EuiObserver abstract base class with a new useObserver hook that manages node ref updates and observer lifecycle/cleanup.
  • Converted EuiResizeObserver and EuiMutationObserver from class components into function components that use useObserver.
  • Added an upcoming changelog entry documenting the internal refactor.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
packages/eui/src/components/observer/resize_observer/resize_observer.tsx Converts EuiResizeObserver to a function component powered by useObserver and refs.
packages/eui/src/components/observer/observer.ts Replaces the EuiObserver class with the useObserver hook and observer lifecycle management.
packages/eui/src/components/observer/mutation_observer/mutation_observer.tsx Converts EuiMutationObserver to a function component powered by useObserver and refs.
packages/eui/changelogs/upcoming/9511.md Adds a changelog entry for the observer refactor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@weronikaolejniczak
Copy link
Contributor

buildkite test this

@ragini-pandey
Copy link
Contributor Author

Thanks for the review! Both comments have been addressed in the latest commit:

1. JSDoc updated — The @description for useObserver now reads "Used by EuiResizeObserver and EuiMutationObserver" and a @param componentName tag is documented.

2. Mount-time ref guard restored — Added an optional componentName parameter (defaults to 'useObserver') and a useEffect that throws "${componentName} did not receive a ref" on mount if the ref callback was never called with an element. This exactly mirrors the old EuiObserver.componentDidMount check. Both EuiResizeObserver and EuiMutationObserver now pass their component names to the 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
- Update JSDoc to mention both EuiResizeObserver and EuiMutationObserver
- Add optional componentName parameter to useObserver
- Add mount-time guard (throws if no ref received), matching EuiObserver.componentDidMount behavior
- Merge cleanup useEffect into the guard effect for clarity
- Pass componentName from EuiResizeObserver and EuiMutationObserver callers
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors EUI’s internal observer components away from an abstract class pattern and into a shared hook, supporting the ongoing class-to-hooks migration effort (issue #9458) while keeping EuiResizeObserver/EuiMutationObserver behavior and exports intact.

Changes:

  • Replaced the EuiObserver abstract class with a shared useObserver(beginObserve, componentName) hook.
  • Converted EuiResizeObserver and EuiMutationObserver from class components to function components using useObserver.
  • Added an upcoming changelog entry describing the internal refactor.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
packages/eui/src/components/observer/observer.ts Introduces useObserver hook to manage ref callback + observer lifecycle/cleanup.
packages/eui/src/components/observer/resize_observer/resize_observer.tsx Migrates EuiResizeObserver to hooks; uses refs to track prior size and latest callback.
packages/eui/src/components/observer/mutation_observer/mutation_observer.tsx Migrates EuiMutationObserver to hooks; uses refs to keep latest mutation callback/options without cycling the ref callback.
packages/eui/changelogs/upcoming/9511.md Documents the observer refactor for the next release notes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors the observer utilities in packages/eui by replacing the EuiObserver abstract base class with a shared useObserver hook, and migrating EuiResizeObserver/EuiMutationObserver from class components to function components.

Changes:

  • Replaced the EuiObserver class with a reusable useObserver(beginObserve, componentName?) hook for ref + lifecycle management.
  • Converted EuiResizeObserver and EuiMutationObserver to function components built on useObserver.
  • Added an upcoming changelog entry documenting the refactor.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
packages/eui/src/components/observer/observer.ts Removes the abstract class and introduces the shared useObserver hook for observer lifecycle/ref handling.
packages/eui/src/components/observer/resize_observer/resize_observer.tsx Migrates EuiResizeObserver to a hook-based function component and uses refs to avoid rerenders.
packages/eui/src/components/observer/mutation_observer/mutation_observer.tsx Migrates EuiMutationObserver to a hook-based function component and stabilizes callback/options via refs.
packages/eui/changelogs/upcoming/9511.md Adds changelog notes for the internal observer refactor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

- Add componentName to useEffect dependency array in useObserver, remove eslint-disable suppression
- Remove React.FunctionComponent annotation from EuiMutationObserver, use typed destructured props instead
- Remove as React.ReactElement type cast from EuiMutationObserver return statement
…nect behavior

The old EuiObserver class called observer.disconnect() twice on unmount:
once in componentWillUnmount and again via the ref callback (called with null).
Each disconnect() calls clearTimeout in the MutationObserver polyfill, so
the count was 9.

The new useObserver hook only disconnects once (via the ref callback), then
sets observerRef to null, so the useEffect cleanup is a no-op. This is the
correct behavior. Update the expected count from 9 to 8.
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Refactors EUI’s observer components by replacing the shared EuiObserver abstract class with a useObserver hook, and migrates EuiResizeObserver/EuiMutationObserver to function components as part of the broader class-to-hooks effort (issue #9458).

Changes:

  • Added a shared useObserver hook to manage ref binding, observer lifecycle, and cleanup.
  • Converted EuiResizeObserver and EuiMutationObserver from class components to function components using useObserver.
  • Updated a popover cleanup test expectation and added an upcoming changelog entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
packages/eui/src/components/observer/observer.ts Replaces EuiObserver base class with the new useObserver hook and preserves the ref-on-mount guard behavior.
packages/eui/src/components/observer/resize_observer/resize_observer.tsx Migrates EuiResizeObserver to a function component using useObserver and refs for dimension tracking.
packages/eui/src/components/observer/mutation_observer/mutation_observer.tsx Migrates EuiMutationObserver to a function component using useObserver, keeping existing mutation observer helpers intact.
packages/eui/src/components/popover/popover.test.tsx Updates timeout cleanup assertion to match new runtime behavior.
packages/eui/changelogs/upcoming/9511.md Documents the internal refactor and component migrations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.


const updateChildNode = useObserver(beginObserve, 'EuiResizeObserver');

return children(updateChildNode) as React.ReactElement;
Comment on lines +25 to +29
export const useObserver = (
beginObserve: (node: Element) => Observer | undefined,
componentName: string = 'useObserver'
) => {
const childNodeRef = useRef<Element | null>(null);
@weronikaolejniczak
Copy link
Contributor

@ragini-pandey additionally, as you can see above, our tests failed ☝🏻 specifically for EuiPopover. Let's make sure popover tests pass and we test all components you've mentioned: accordion, context menu panel, markdown editor, tooltip, make sure they behave correctly 😄 If you need any help, let me know!


unmount();
expect(window.clearTimeout).toHaveBeenCalledTimes(9);
expect(window.clearTimeout).toHaveBeenCalledTimes(8);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it expected that clearTimeout is called 8 times and not 9?

@weronikaolejniczak
Copy link
Contributor

@ragini-pandey since we are removing EuiObserver and adding useObserver, we should also update the documentation in packages/website. Instead of being in "Components", it should be documented in "Utilities".

@weronikaolejniczak
Copy link
Contributor

buildkite test this

@elasticmachine
Copy link
Collaborator

💔 Build Failed

Failed CI Steps

History

@elasticmachine
Copy link
Collaborator

💚 Build Succeeded

History

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community contribution (Don't delete - used for automation)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[EuiObserver] Migrate abstract base class to a hook

4 participants