File name
Commit message
Commit date
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 "../_lib/read-tokens";
import { THEME_ATTRIBUTE_NAME } from "@/lib/constants/theme";
// CSSOM은 React 바깥의 외부 시스템이라 `useSyncExternalStore`가 맞는 도구다
// (effect에서 setState 하면 렌더가 연쇄된다).
//
// 토큰 **값**은 현재 테마에 따라 달라지므로 `data-theme`이 바뀌면 다시 읽어야 한다.
// 반면 `useSyncExternalStore`는 매 렌더마다 getSnapshot을 호출하므로 매번 새 객체를
// 돌려주면 무한 루프가 된다 — 그래서 테마 키로 캐시해 같은 테마 동안에는 동일한
// 참조를 반환한다.
let cached: { theme: string; registry: TokenRegistry } | null = null;
function currentTheme(): string {
return document.documentElement.getAttribute(THEME_ATTRIBUTE_NAME) ?? "system";
}
function subscribe(onChange: () => void): () => void {
const observer = new MutationObserver(onChange);
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: [THEME_ATTRIBUTE_NAME],
});
return () => observer.disconnect();
}
function getSnapshot(): TokenRegistry | null {
const theme = currentTheme();
if (!cached || cached.theme !== theme) {
cached = { theme, registry: readTokenRegistry() };
}
return cached.registry;
}
function getServerSnapshot(): TokenRegistry | null {
return null;
}
/**
* 서버 렌더와 하이드레이션 첫 렌더에서는 `null`, 그 이후 토큰 레지스트리를 반환한다.
* 테마를 바꾸면 그 테마에서 해석된 값으로 갱신된다.
*/
export function useTokenRegistry(): TokenRegistry | null {
return useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
}