diff --git a/src/internal/index.ts b/src/internal/index.ts
index 442f8a6..7aab1e2 100644
--- a/src/internal/index.ts
+++ b/src/internal/index.ts
@@ -37,3 +37,4 @@ export { isFocusable, getAllFocusables, getFirstFocusable, getLastFocusable } fr
export { default as handleKey } from './utils/handle-key';
export { default as circleIndex } from './utils/circle-index';
export { default as Portal, PortalProps } from './portal';
+export { useMergeRefs } from './use-merge-refs';
diff --git a/src/internal/use-merge-refs/__tests__/use-merge-refs.test.tsx b/src/internal/use-merge-refs/__tests__/use-merge-refs.test.tsx
new file mode 100644
index 0000000..569d775
--- /dev/null
+++ b/src/internal/use-merge-refs/__tests__/use-merge-refs.test.tsx
@@ -0,0 +1,52 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React from 'react';
+import { render } from '@testing-library/react';
+
+import { useMergeRefs } from '../index';
+
+const DemoNull = React.forwardRef((props, ref) => {
+ const mergedRef = useMergeRefs(null, ref, undefined);
+ return (
+ <>
+
+ >
+ );
+});
+
+const Demo = React.forwardRef((props, ref) => {
+ const ref2 = React.createRef();
+ const mergedRef = useMergeRefs(ref, ref2);
+ return (
+ <>
+
+ >
+ );
+});
+
+describe('use merge refs', function () {
+ it('does not cause component to crash when all refs are null or undefined', () => {
+ render();
+ expect(document.querySelector('.target')).not.toBe(null);
+ });
+
+ it('merges ref with null refs', () => {
+ const ref1 = React.createRef();
+ render();
+ expect(ref1.current!.classList).toContain('target');
+ });
+
+ it('merges two refs', () => {
+ const ref1 = React.createRef();
+ render();
+ expect(ref1.current!.classList).toContain('target');
+ });
+
+ it('ref callback has been called', () => {
+ const ref1 = jest.fn();
+ render();
+ expect(ref1).toHaveBeenCalledTimes(1);
+ expect(ref1).toHaveBeenCalledWith(expect.objectContaining({ className: 'target' }));
+ });
+});
diff --git a/src/internal/use-merge-refs/index.tsx b/src/internal/use-merge-refs/index.tsx
new file mode 100644
index 0000000..92deb0f
--- /dev/null
+++ b/src/internal/use-merge-refs/index.tsx
@@ -0,0 +1,32 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+import React, { useMemo } from 'react';
+
+/**
+ * useMergeRefs merges multiple refs into single ref callback.
+ *
+ * For example
+ * const mergedRef = useMergeRefs(ref1, ref2, ref3)
+ * ...
+ */
+export function useMergeRefs(
+ ...refs: Array | React.MutableRefObject | null | undefined>
+): React.RefCallback | null {
+ return useMemo(() => {
+ if (refs.every(ref => ref === null || ref === undefined)) {
+ return null;
+ }
+ return (value: T | null) => {
+ refs.forEach(ref => {
+ if (typeof ref === 'function') {
+ ref(value);
+ } else if (ref !== null && ref !== undefined) {
+ (ref as React.MutableRefObject).current = value;
+ }
+ });
+ };
+ // ESLint expects an array literal which we can not provide here
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, refs);
+}