Smooth locale switches with a React transition
Schedule persist and navigate together, and defer the mismatch banner so a one-frame URL/identity desync does not flash during language changes from the menu. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
@@ -22,7 +22,7 @@ import { GlobeIcon, XIcon } from "@phosphor-icons/react";
|
||||
import { Button } from "@probo/ui/src/v2/Button/Button";
|
||||
import { IconButton } from "@probo/ui/src/v2/IconButton/IconButton";
|
||||
import { Text } from "@probo/ui/src/v2/typography/Text";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useDeferredValue, useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { graphql, useFragment } from "react-relay";
|
||||
|
||||
@@ -63,7 +63,11 @@ export function LocaleMismatchCallout({ identityKey }: LocaleMismatchCalloutProp
|
||||
|
||||
const savedLocale = isUrlLocale(identity.locale) ? identity.locale : null;
|
||||
const savedLanguage = savedLocale != null ? urlLocaleToLanguage(savedLocale) : null;
|
||||
const visible = !dismissed && savedLocale != null && savedLocale !== urlLocale;
|
||||
const mismatched = savedLocale != null && savedLocale !== urlLocale;
|
||||
// Lag the mismatch flag so a transient desync during startTransition locale
|
||||
// switches never paints the banner; a real mismatch still shows once settled.
|
||||
const deferredMismatched = useDeferredValue(mismatched);
|
||||
const visible = !dismissed && mismatched && deferredMismatched;
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible || savedLanguage == null) {
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
|
||||
import { useCallback } from "react";
|
||||
import { startTransition, useCallback } from "react";
|
||||
import { useLocation, useNavigate } from "react-router";
|
||||
|
||||
import {
|
||||
@@ -40,23 +40,21 @@ export function useChangeLocale() {
|
||||
const navigate = useNavigate();
|
||||
const [updateLocale, isUpdating] = useUpdateLocale();
|
||||
|
||||
const changeLocale = useCallback(async (
|
||||
const changeLocale = useCallback((
|
||||
locale: UrlLocale,
|
||||
options: ChangeLocaleOptions = {},
|
||||
) => {
|
||||
// Persist and navigate without sequencing them: awaiting the mutation
|
||||
// before navigation left a frame where Identity.locale already matched
|
||||
// the new choice but the URL still had the old prefix, flashing the
|
||||
// mismatch callout. updateLocale writes the store optimistically, and
|
||||
// flushSync applies the URL change in the same paint.
|
||||
if (options.persist) {
|
||||
void updateLocale(locale);
|
||||
}
|
||||
if (locale !== currentLocale) {
|
||||
void navigate(replaceLocaleInPathname(pathname, locale) + search, {
|
||||
flushSync: true,
|
||||
});
|
||||
}
|
||||
// Persist + navigate as one transition so React can keep the previous UI
|
||||
// until both have settled. Await-then-navigate painted a one-frame
|
||||
// Identity↔URL desync that flashed the mismatch callout.
|
||||
startTransition(() => {
|
||||
if (options.persist) {
|
||||
void updateLocale(locale);
|
||||
}
|
||||
if (locale !== currentLocale) {
|
||||
void navigate(replaceLocaleInPathname(pathname, locale) + search);
|
||||
}
|
||||
});
|
||||
}, [currentLocale, navigate, pathname, search, updateLocale]);
|
||||
|
||||
return [changeLocale, isUpdating] as const;
|
||||
|
||||
@@ -53,11 +53,6 @@ export function useUpdateLocale() {
|
||||
await commit(
|
||||
{
|
||||
variables: { input: { locale } },
|
||||
// Keep the Relay store in sync with the URL during locale switches so
|
||||
// the mismatch callout never paints a one-frame desync.
|
||||
optimisticUpdater: (store) => {
|
||||
store.getRoot().getLinkedRecord("viewer")?.setValue(locale, "locale");
|
||||
},
|
||||
},
|
||||
feedback,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user