Fix cookie banner disappearing when TinaCloud data loads
Refactored LanguageProvider to avoid remounting children when switching from static to live translations. Previously, the switch from LanguageContext.Provider to TinaConnectedProvider caused all children to unmount/remount, resetting CookieConsent state and hiding the banner if cookie_consent was already set in localStorage. New approach: TinaLiveSync is a null-rendering component that calls useTina and syncs live data back to LanguageProvider via a stable callback, while LanguageContext.Provider remains the stable root wrapper — children never remount. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
09943e9f5e
commit
2ab22e5efb
1 changed files with 44 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode } from 'react';
|
||||
import { createContext, useContext, useState, useCallback, useMemo, useEffect, useRef, type ReactNode } from 'react';
|
||||
import { useTina } from 'tinacms/dist/react';
|
||||
import client from '../../tina/__generated__/client';
|
||||
import type { Lang, TranslationKey, Translations } from './types';
|
||||
|
|
@ -9,7 +9,6 @@ function flattenObject(obj: Record<string, unknown>, prefix = ''): Record<string
|
|||
return Object.entries(obj).reduce((acc, [key, val]) => {
|
||||
const fullKey = prefix ? `${prefix}.${key}` : key;
|
||||
if (key === '_') {
|
||||
// Special case: _ maps the value to the parent key path
|
||||
acc[prefix] = String(val);
|
||||
} else if (typeof val === 'object' && val !== null) {
|
||||
Object.assign(acc, flattenObject(val as Record<string, unknown>, fullKey));
|
||||
|
|
@ -43,20 +42,17 @@ function getInitialLang(): Lang {
|
|||
|
||||
type TinaQueryResult = { query: string; variables: Record<string, unknown>; data: Record<string, unknown> };
|
||||
|
||||
// Inner component — only rendered when both TinaCloud queries are ready.
|
||||
// useTina is never called with an empty query.
|
||||
function TinaConnectedProvider({
|
||||
// Headless component — calls useTina (which requires valid queries) and syncs
|
||||
// live data back to LanguageProvider via a stable callback. Renders nothing,
|
||||
// so swapping it in/out never remounts the page children.
|
||||
function TinaLiveSync({
|
||||
enResult,
|
||||
ukResult,
|
||||
lang,
|
||||
setLang,
|
||||
children,
|
||||
onSync,
|
||||
}: {
|
||||
enResult: TinaQueryResult;
|
||||
ukResult: TinaQueryResult;
|
||||
lang: Lang;
|
||||
setLang: (l: Lang) => void;
|
||||
children: ReactNode;
|
||||
onSync: (enData: unknown, ukData: unknown) => void;
|
||||
}) {
|
||||
const { data: enLiveData } = useTina({
|
||||
query: enResult.query,
|
||||
|
|
@ -70,6 +66,7 @@ function TinaConnectedProvider({
|
|||
data: ukResult.data,
|
||||
});
|
||||
|
||||
// Apply design tokens from EN translations
|
||||
useEffect(() => {
|
||||
const design = (enLiveData as any).translationsEn?.design;
|
||||
if (!design) return;
|
||||
|
|
@ -81,25 +78,19 @@ function TinaConnectedProvider({
|
|||
if (design.colorText) s.setProperty('--light-grey-100', design.colorText);
|
||||
}, [enLiveData]);
|
||||
|
||||
const liveTranslations = useMemo(() => ({
|
||||
en: flattenObject((enLiveData as any).translationsEn ?? enResult.data) as unknown as Translations,
|
||||
uk: flattenObject((ukLiveData as any).translationsUk ?? ukResult.data) as unknown as Translations,
|
||||
}), [enLiveData, ukLiveData, enResult.data, ukResult.data]);
|
||||
// Sync live translation data back to parent
|
||||
useEffect(() => {
|
||||
onSync(enLiveData, ukLiveData);
|
||||
}, [enLiveData, ukLiveData, onSync]);
|
||||
|
||||
const t = useCallback(
|
||||
(key: TranslationKey): string => liveTranslations[lang][key] ?? liveTranslations.en[key] ?? key,
|
||||
[lang, liveTranslations]
|
||||
);
|
||||
|
||||
const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]);
|
||||
|
||||
return <LanguageContext.Provider value={value}>{children}</LanguageContext.Provider>;
|
||||
return null;
|
||||
}
|
||||
|
||||
export function LanguageProvider({ children }: { children: ReactNode }) {
|
||||
const [lang, setLangState] = useState<Lang>(getInitialLang);
|
||||
const [enResult, setEnResult] = useState<TinaQueryResult | null>(null);
|
||||
const [ukResult, setUkResult] = useState<TinaQueryResult | null>(null);
|
||||
const [liveTranslations, setLiveTranslations] = useState<Record<Lang, Translations> | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
client.queries.translationsEn({ relativePath: 'en.json' })
|
||||
|
|
@ -118,22 +109,39 @@ export function LanguageProvider({ children }: { children: ReactNode }) {
|
|||
|
||||
document.documentElement.lang = lang;
|
||||
|
||||
// Before TinaCloud data loads — use static translations (no useTina calls)
|
||||
const staticT = useCallback(
|
||||
(key: TranslationKey): string => staticTranslations[lang][key] ?? staticTranslations.en[key] ?? key,
|
||||
[lang]
|
||||
// Stable callback — uses ref to avoid re-creating TinaLiveSync's effect deps
|
||||
const setLiveRef = useRef(setLiveTranslations);
|
||||
setLiveRef.current = setLiveTranslations;
|
||||
|
||||
const handleSync = useCallback((enData: unknown, ukData: unknown) => {
|
||||
setLiveRef.current({
|
||||
en: flattenObject((enData as any).translationsEn ?? {}) as unknown as Translations,
|
||||
uk: flattenObject((ukData as any).translationsUk ?? {}) as unknown as Translations,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const translations = liveTranslations ?? staticTranslations;
|
||||
|
||||
const t = useCallback(
|
||||
(key: TranslationKey): string => translations[lang][key] ?? translations.en[key] ?? key,
|
||||
[lang, translations]
|
||||
);
|
||||
const staticValue = useMemo(() => ({ lang, setLang, t: staticT }), [lang, setLang, staticT]);
|
||||
|
||||
if (enResult && ukResult) {
|
||||
return (
|
||||
<TinaConnectedProvider enResult={enResult} ukResult={ukResult} lang={lang} setLang={setLang}>
|
||||
{children}
|
||||
</TinaConnectedProvider>
|
||||
);
|
||||
}
|
||||
const value = useMemo(() => ({ lang, setLang, t }), [lang, setLang, t]);
|
||||
|
||||
return <LanguageContext.Provider value={staticValue}>{children}</LanguageContext.Provider>;
|
||||
return (
|
||||
<LanguageContext.Provider value={value}>
|
||||
{/* TinaLiveSync is null-rendering — mounts/unmounts without affecting children */}
|
||||
{enResult && ukResult && (
|
||||
<TinaLiveSync
|
||||
enResult={enResult}
|
||||
ukResult={ukResult}
|
||||
onSync={handleSync}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</LanguageContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useTranslation(): LanguageContextValue {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue