Utilities for working with React refs, including merging multiple refs and creating effect-based refs.
React hook to merge multiple React refs (either MutableRefObjects or ref callbacks) into a single ref callback that updates all provided refs. Essential for forwarding refs while also maintaining internal refs.
/**
* React hook to merge multiple React refs into a single ref callback that updates all provided refs
* @param refs - Refs to collectively update with one ref value.
* @returns A function with an attached "current" prop, so that it can be treated like a RefObject.
*/
function useMergedRefs<T>(...refs: (React.Ref<T> | undefined)[]): RefObjectFunction<T>;
/**
* A Ref function which can be treated like a ref object in that it has an attached
* current property, which will be updated as the ref is evaluated.
*/
type RefObjectFunction<T> = React.RefObject<T> & ((value: T) => void);Usage Examples:
import { useMergedRefs } from "@fluentui/react-hooks";
import { forwardRef } from "react";
// Basic ref forwarding with internal ref
const CustomInput = forwardRef<HTMLInputElement, { label: string }>((props, ref) => {
const internalRef = useRef<HTMLInputElement>(null);
const mergedRef = useMergedRefs(ref, internalRef);
const focusInput = () => {
internalRef.current?.focus();
};
return (
<div>
<label>{props.label}</label>
<input ref={mergedRef} />
<button onClick={focusInput}>Focus Input</button>
</div>
);
});
// Multiple ref callbacks
function MultiRefComponent() {
const ref1 = useRef<HTMLDivElement>(null);
const ref2 = useRef<HTMLDivElement>(null);
const callbackRef1 = useCallback((element: HTMLDivElement | null) => {
if (element) {
console.log('Callback ref 1:', element);
}
}, []);
const callbackRef2 = useCallback((element: HTMLDivElement | null) => {
if (element) {
console.log('Callback ref 2:', element);
}
}, []);
const mergedRef = useMergedRefs(ref1, ref2, callbackRef1, callbackRef2);
return (
<div ref={mergedRef}>
This element is referenced by multiple refs
</div>
);
}
// HOC pattern with ref forwarding
function withLogging<T extends HTMLElement>(Component: React.ComponentType<any>) {
return forwardRef<T, any>((props, ref) => {
const loggingRef = useCallback((element: T | null) => {
if (element) {
console.log('Element attached:', element.tagName);
} else {
console.log('Element detached');
}
}, []);
const mergedRef = useMergedRefs(ref, loggingRef);
return <Component {...props} ref={mergedRef} />;
});
}Creates a ref, and calls a callback whenever the ref changes to a non-null value. The callback can optionally return a cleanup function that'll be called before the value changes, and when the ref is unmounted. This works around the limitation that useEffect cannot depend on ref.current.
/**
* Creates a ref, and calls a callback whenever the ref changes to a non-null value.
* The callback can optionally return a cleanup function.
* @param callback - Called whenever the ref's value changes to non-null. Can optionally return a cleanup function.
* @param initial - (Optional) The initial value for the ref.
* @returns A function that should be called to set the ref's value. Also has a .current member for access.
*/
function useRefEffect<T>(
callback: (value: T) => (() => void) | void,
initial?: T | null
): RefCallback<T>;
/**
* A callback ref function that also has a .current member for the ref's current value.
*/
type RefCallback<T> = ((value: T | null) => void) & React.RefObject<T>;Usage Examples:
import { useRefEffect } from "@fluentui/react-hooks";
// Basic DOM manipulation on element attach/detach
function AutoFocusComponent() {
const inputRef = useRefEffect<HTMLInputElement>(
(element) => {
// Called when element is attached
element.focus();
// Return cleanup function (optional)
return () => {
console.log('Input element is being detached');
};
}
);
return <input ref={inputRef} placeholder="Auto-focused" />;
}
// Intersection Observer setup
function VisibilityTracker({ onVisibilityChange }) {
const elementRef = useRefEffect<HTMLDivElement>(
(element) => {
const observer = new IntersectionObserver(
(entries) => {
const isVisible = entries[0].isIntersecting;
onVisibilityChange(isVisible);
},
{ threshold: 0.1 }
);
observer.observe(element);
// Cleanup: disconnect observer
return () => {
observer.disconnect();
};
}
);
return (
<div ref={elementRef} style={{ height: '200px', background: 'lightblue' }}>
Visibility tracked element
</div>
);
}
// Resize Observer
function ResizeTracker() {
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
const elementRef = useRefEffect<HTMLDivElement>(
(element) => {
const resizeObserver = new ResizeObserver((entries) => {
const { width, height } = entries[0].contentRect;
setDimensions({ width, height });
});
resizeObserver.observe(element);
return () => {
resizeObserver.disconnect();
};
}
);
return (
<div>
<div
ref={elementRef}
style={{
resize: 'both',
overflow: 'auto',
border: '1px solid #ccc',
minWidth: '100px',
minHeight: '100px'
}}
>
Resizable element
</div>
<div>Dimensions: {dimensions.width} x {dimensions.height}</div>
</div>
);
}
// Canvas setup with cleanup
function CanvasComponent() {
const canvasRef = useRefEffect<HTMLCanvasElement>(
(canvas) => {
const context = canvas.getContext('2d');
if (!context) return;
// Setup canvas
canvas.width = 400;
canvas.height = 300;
// Draw something
context.fillStyle = 'blue';
context.fillRect(10, 10, 100, 100);
// Animation loop
let animationId: number;
const animate = () => {
// Animation logic here
animationId = requestAnimationFrame(animate);
};
animate();
// Cleanup animation
return () => {
cancelAnimationFrame(animationId);
};
}
);
return <canvas ref={canvasRef} />;
}
// Third-party library integration
function ChartComponent({ data }) {
const chartRef = useRefEffect<HTMLDivElement>(
(element) => {
// Initialize chart library
const chart = new SomeChartLibrary(element, {
data,
responsive: true
});
// Cleanup chart instance
return () => {
chart.destroy();
};
}
);
return <div ref={chartRef} style={{ width: '100%', height: '400px' }} />;
}