Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 55 additions & 28 deletions semcore/core/src/utils/use/useScrollBarWidth.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,66 @@
import { useEffect, useState, useRef } from 'react';
import { useSyncExternalStore } from 'react';

@Valeria-Zimnitskaya Valeria-Zimnitskaya Aug 24, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@slizhevskyv-semrush useSyncExternalStore was introduced in React 18, while @semcore/core currently declares support for React 16.8–19. Importing it directly from react will break consumers using React 16 or 17.
Could we use the backwards-compatible something? (if i correct))

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

yeah, good point! Thanks!
My assumption was that we support the current version -1. I will take care about it :)


export function useScrollBarWidth(vertical = true): number {
const [scrollBarWidth, setScrollBarWidth] = useState(0);
const af = useRef<number | null>(null);
type Dimension = 'vertical' | 'horizontal';

const state: Record<Dimension, number> = { vertical: 0, horizontal: 0 };
const listeners = new Set<() => void>();

let rafId: number | null = null;
let inited = false;

function measure() {
if (!window.visualViewport) return;

const nextVertical = window.innerWidth - window.visualViewport.width;
const nextHorizontal = window.innerHeight - window.visualViewport.height;

useEffect(() => {
const calculateScrollBar = () => {
if (!window.visualViewport) return;
if (nextVertical === state.vertical && nextHorizontal === state.horizontal) return;

if (!vertical) {
setScrollBarWidth(window.innerHeight - window.visualViewport.height);
return;
}
state.vertical = nextVertical;
state.horizontal = nextHorizontal;

setScrollBarWidth(window.innerWidth - window.visualViewport.width);
};
listeners.forEach((l) => l());
}

function handleResize() {
if (rafId !== null) return;

rafId = requestAnimationFrame(() => {
measure();
rafId = null;
});
}

const handleResize = () => {
// to handle resize 1 time per frame
if (af.current !== null) return;
function init() {
if (inited) return;

af.current = requestAnimationFrame(() => {
calculateScrollBar();
af.current = null;
});
};
inited = true;

calculateScrollBar();
window.addEventListener('resize', handleResize);
measure();

return () => {
window.addEventListener('resize', handleResize);
}

function subscribe(onStoreChange: () => void) {
init();

listeners.add(onStoreChange);

return () => {
listeners.delete(onStoreChange);

if (listeners.size === 0) {
window.removeEventListener('resize', handleResize);
if (af.current !== null) cancelAnimationFrame(af.current);
};
}, []);
}
};
}

export function useScrollBarWidth(vertical = true): number {
const dim: Dimension = vertical ? 'vertical' : 'horizontal';

return scrollBarWidth;
return useSyncExternalStore(
subscribe,
() => state[dim],
() => 0,
);
}
Loading