A React hook that schedules a `React.useLayoutEffect` with a fallback to a `React.useEffect` for environments where layout effects should not be used (such as server-side rendering).
npm install @utilityjs/use-isomorphic-layout-effectA React hook that schedules a React.useLayoutEffect with a fallback to aReact.useEffect for environments where layout effects should not be used (such
as server-side rendering).
- SSR Safe: Prevents hydration mismatches and SSR warnings
- Client Optimization: Uses useLayoutEffect for synchronous DOM updates on
client
- Server Compatibility: Falls back to useEffect on server environments
- Drop-in Replacement: Same API as useLayoutEffect
- TypeScript Support: Full type safety with proper TypeScript definitions
- Zero Runtime Cost: Simple conditional export with no overhead
``bash`
npm install @utilityjs/use-isomorphic-layout-effect
or
`bash`
pnpm add @utilityjs/use-isomorphic-layout-effect
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef, useState } from "react";
function MeasuredComponent() {
const ref = useRef
const [dimensions, setDimensions] = useState({ width: 0, height: 0 });
useIsomorphicLayoutEffect(() => {
if (ref.current) {
// This runs synchronously after DOM mutations on client
// but as regular effect on server to avoid SSR issues
const { offsetWidth, offsetHeight } = ref.current;
setDimensions({ width: offsetWidth, height: offsetHeight });
}
}, []);
return (
Width: {dimensions.width}px
Height: {dimensions.height}px
$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef } from "react";function FocusedInput({ autoFocus }: { autoFocus?: boolean }) {
const inputRef = useRef(null);
useIsomorphicLayoutEffect(() => {
if (autoFocus && inputRef.current) {
// Focus input synchronously on client, safely on server
inputRef.current.focus();
}
}, [autoFocus]);
return (
ref={inputRef}
placeholder="Type here..."
/>
);
}
`$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRouter } from "next/router";function ScrollToTop() {
const router = useRouter();
useIsomorphicLayoutEffect(() => {
// Scroll to top on route change, but only on client
if (typeof window !== "undefined") {
window.scrollTo(0, 0);
}
}, [router.pathname]);
return null;
}
`$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef, useState } from "react";function ResponsiveText() {
const containerRef = useRef(null);
const [fontSize, setFontSize] = useState(16);
useIsomorphicLayoutEffect(() => {
const updateFontSize = () => {
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth;
// Adjust font size based on container width
const newFontSize = Math.max(12, Math.min(24, containerWidth / 20));
setFontSize(newFontSize);
}
};
updateFontSize();
window.addEventListener("resize", updateFontSize);
return () => window.removeEventListener("resize", updateFontSize);
}, []);
return (
ref={containerRef}
style={{ width: "100%", border: "1px solid #ccc" }}
>
${fontSize}px, margin: 0 }}>
This text scales with container width
$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef, useState } from "react";function AnimatedBox() {
const boxRef = useRef(null);
const [isVisible, setIsVisible] = useState(false);
useIsomorphicLayoutEffect(() => {
if (boxRef.current) {
// Set initial state synchronously to prevent flash
boxRef.current.style.transform = "translateY(20px)";
boxRef.current.style.opacity = "0";
// Trigger animation
const timer = setTimeout(() => {
if (boxRef.current) {
boxRef.current.style.transition = "all 0.3s ease";
boxRef.current.style.transform = "translateY(0)";
boxRef.current.style.opacity = "1";
setIsVisible(true);
}
}, 100);
return () => clearTimeout(timer);
}
}, []);
return (
ref={boxRef}
style={{
width: "200px",
height: "100px",
backgroundColor: "#007bff",
color: "white",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{isVisible ? "Animated!" : "Loading..."}
$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef } from "react";function Canvas() {
const canvasRef = useRef(null);
useIsomorphicLayoutEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
// Set canvas size to match display size
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * window.devicePixelRatio;
canvas.height = rect.height * window.devicePixelRatio;
ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
// Draw something
ctx.fillStyle = "#007bff";
ctx.fillRect(10, 10, 100, 50);
ctx.fillStyle = "white";
ctx.font = "16px Arial";
ctx.fillText("Canvas", 20, 35);
}, []);
return (
ref={canvasRef}
style={{ width: "200px", height: "100px", border: "1px solid #ccc" }}
/>
);
}
`$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef } from "react";function ChartComponent({ data }: { data: number[] }) {
const chartRef = useRef(null);
useIsomorphicLayoutEffect(() => {
if (!chartRef.current || typeof window === "undefined") return;
// Dynamically import chart library only on client
import("chart.js").then(({ Chart, registerables }) => {
Chart.register(...registerables);
const canvas = document.createElement("canvas");
chartRef.current!.appendChild(canvas);
const chart = new Chart(canvas, {
type: "line",
data: {
labels: data.map((_, i) =>
Point ${i + 1}),
datasets: [
{
label: "Data",
data: data,
borderColor: "#007bff",
backgroundColor: "rgba(0, 123, 255, 0.1)",
},
],
},
}); return () => chart.destroy();
});
}, [data]);
return (
ref={chartRef}
style={{ width: "400px", height: "200px" }}
/>
);
}
`$3
`tsx
import { useIsomorphicLayoutEffect } from "@utilityjs/use-isomorphic-layout-effect";
import { useRef, useState } from "react";function useElementSize() {
const ref = useRef(null);
const [size, setSize] = useState({ width: 0, height: 0 });
useIsomorphicLayoutEffect(() => {
const element = ref.current;
if (!element) return;
const updateSize = () => {
setSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
};
// Initial measurement
updateSize();
// Set up ResizeObserver if available
if (typeof ResizeObserver !== "undefined") {
const resizeObserver = new ResizeObserver(updateSize);
resizeObserver.observe(element);
return () => resizeObserver.disconnect();
}
// Fallback to window resize
window.addEventListener("resize", updateSize);
return () => window.removeEventListener("resize", updateSize);
}, []);
return { ref, size };
}
function SizeAwareComponent() {
const { ref, size } = useElementSize();
return (
ref={ref}
style={{
width: "50%",
minHeight: "100px",
backgroundColor: "#f0f0f0",
padding: "20px",
resize: "both",
overflow: "auto",
}}
>
Width: {size.width}px
Height: {size.height}px
Resize me!
API
$3
A hook that uses
useLayoutEffect on the client and useEffect on the server.#### Parameters
-
effect (EffectCallback): The effect function to run
- deps (DependencyList, optional): Dependency array for the effect#### Returns
-
void: No return value#### Example
`tsx
useIsomorphicLayoutEffect(() => {
// Effect code here
return () => {
// Cleanup code here
};
}, [dependency]);
`When to Use
$3
- DOM Measurements: Reading layout properties like
offsetWidth,
scrollTop`- Data Fetching: Async operations that don't affect layout
- Event Listeners: Setting up non-layout related listeners
- Timers: setTimeout/setInterval that don't affect DOM
- Side Effects: Operations that don't need to be synchronous
- Hydration Safety: Prevents React hydration warnings
- Performance: Avoids unnecessary work on the server
- Compatibility: Works with Next.js, Gatsby, and other SSR frameworks
- Progressive Enhancement: Gracefully handles server-to-client transition
Read the
contributing guide
to learn about our development process, how to propose bug fixes and
improvements, and how to build and test your changes.
This project is licensed under the terms of the
MIT license.