File name
Commit message
Commit date
File name
Commit message
Commit date
File name
Commit message
Commit date
import { useSyncExternalStore } from "react";
import { readTokenRegistry, type TokenRegistry } from "./read-tokens";
// CSSOM은 React 바깥의 외부 시스템이라 `useSyncExternalStore`가 맞는 도구다
// (effect에서 setState 하면 렌더가 연쇄된다).
//
// 토큰 **값**은 테마와 뷰포트에 따라 달라지므로 둘 중 하나가 바뀌면 다시 읽어야 한다.
// 반면 `useSyncExternalStore`는 매 렌더마다 getSnapshot을 호출하므로 매번 새 객체를
// 돌려주면 무한 루프가 된다 — 그래서 (테마, 뷰포트 구간)을 키로 캐시해 같은 조건
// 동안에는 동일한 참조를 반환한다.
let cached: { key: string; registry: TokenRegistry } | null = null;
/** `@fox/styles/_root.scss`가 읽는 수동 테마 선택 속성. */
export const THEME_ATTRIBUTE = "data-theme";
function snapshotKey(): string {
const theme = document.documentElement.getAttribute(THEME_ATTRIBUTE) ?? "system";
// 폭 자체가 아니라 "반응형 토큰이 바뀌는 구간"만 키에 넣는다 — 창을 1px 줄일 때마다
// 전체 토큰을 다시 읽으면 낭비다.
const wide = window.matchMedia("(min-width: 768px)").matches ? "pc" : "mobile";
return `${theme}:${wide}`;
}
function subscribe(onChange: () => void): () => void {
const observer = new MutationObserver(onChange);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: [THEME_ATTRIBUTE],
});
const query = window.matchMedia("(min-width: 768px)");
query.addEventListener("change", onChange);
return () => {
observer.disconnect();
query.removeEventListener("change", onChange);
};
}
function getSnapshot(): TokenRegistry | null {
const key = snapshotKey();
if (!cached || cached.key !== key) {
cached = { key, registry: readTokenRegistry() };
}
return cached.registry;
}
function getServerSnapshot(): TokenRegistry | null {
return null;
}
/**
* 서버 렌더와 하이드레이션 첫 렌더에서는 `null`, 그 이후 토큰 레지스트리를 반환한다.
* 테마를 바꾸거나 768px 경계를 넘으면 그 조건에서 해석된 값으로 갱신된다.
*/
export function useTokenRegistry(): TokenRegistry | null {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}