Skip to content

Commit

Permalink
Fixing issue with re rendering using local storage (#1938)
Browse files Browse the repository at this point in the history
* Fix for re-rendering with progress changes

* rename setProgress to updateProgress
  • Loading branch information
oliverabrahams authored Feb 12, 2025
1 parent 87acb88 commit 80a747e
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 110 deletions.
6 changes: 3 additions & 3 deletions libs/@guardian/react-crossword/src/components/Controls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ const CheckGrid = (props: ButtonProps) => {

const RevealGrid = (props: ButtonProps) => {
const { cells } = useData();
const { setProgress } = useProgress();
const { updateProgress } = useProgress();

const reveal = useCallback(() => {
const newProgress: Progress = [];
Expand All @@ -212,8 +212,8 @@ const RevealGrid = (props: ButtonProps) => {
column[cell.y] = cell.solution ?? '';
}

setProgress(newProgress);
}, [cells, setProgress]);
updateProgress(newProgress);
}, [cells, updateProgress]);
return (
<CrosswordButton
onClick={reveal}
Expand Down
8 changes: 0 additions & 8 deletions libs/@guardian/react-crossword/src/components/Grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,6 @@ export const Grid = () => {
const { currentCell, setCurrentCell } = useCurrentCell();
const { currentEntryId, setCurrentEntryId } = useCurrentClue();
const [focused, setFocused] = useState(false);
const [, setHydrated] = useState(false);

const gridRef = useRef<SVGSVGElement>(null);
const workingDirectionRef = useRef<Direction>('across');
Expand Down Expand Up @@ -390,13 +389,6 @@ export const Grid = () => {
[],
);

/**
* This forces re-rendering of the crossword when hydrated in Preact, which works in React
*/
useEffect(() => {
setHydrated(true);
}, []);

// Handle changes to the current cell
useEffect(() => {
// If the current cell changes, we need to update the current entry ID
Expand Down
61 changes: 47 additions & 14 deletions libs/@guardian/react-crossword/src/context/Progress.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { log } from '@guardian/libs';
import type { Dispatch, SetStateAction } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { createContext, type ReactNode, useContext } from 'react';
import type { LocalStorageOptions } from 'use-local-storage-state';
import useLocalStorageState from 'use-local-storage-state';
import type { CAPICrossword } from '../@types/CAPI';
import type { Dimensions, Progress } from '../@types/crossword';
import { useStoredState } from '../hooks/useStoredState';
import { getNewProgress } from '../utils/getNewProgress';

export const serializer: LocalStorageOptions<unknown>['serializer'] = {
stringify: (_) => JSON.stringify({ value: _ }),
parse: (_) => (JSON.parse(_) as { value: unknown }).value,
};

const isValid = (
progress: unknown,
{ dimensions }: { dimensions: Dimensions },
Expand Down Expand Up @@ -68,7 +74,7 @@ const getInitialProgress = ({

type Context = {
progress: Progress;
setProgress: Dispatch<SetStateAction<Progress | undefined>>;
updateProgress: (newProgress: Progress) => void;
isStored: boolean;
};

Expand All @@ -85,19 +91,46 @@ export const ProgressProvider = ({
progress?: Progress;
children: ReactNode;
}) => {
const [progress, setProgress, { isPersistent }] = useStoredState(id, {
defaultValue: getInitialProgress({ id, dimensions, userProgress }),
validator: (progress: unknown) => isValid(progress, { dimensions }),
});
const defaultValue = getInitialProgress({ id, dimensions, userProgress });

// The localStorage state (managed by useLocalStorageState) does not use React state
// so updates to it do not always trigger a re-render - this behavior is particularly noticeable in Preact.
// To ensure the UI is kept up-to-date, we maintain a separate React state (`progress`) that mirrors
// the localStorage state and forces the necessary re-renders.
const [progress, setProgress] = useState(defaultValue);
const [storedProgress, setStoredProgress, rest] =
useLocalStorageState<Progress>(id, {
defaultValue,
serializer,
});

const updateProgress = useCallback(
(newProgress: Progress) => {
setStoredProgress(newProgress);
setProgress(newProgress);
},
[setStoredProgress],
);

useEffect(() => {
if (isValid(storedProgress, { dimensions })) {
setProgress(storedProgress);
} else {
updateProgress(defaultValue);
}
}, [defaultValue, dimensions, storedProgress, updateProgress]);

const contextValue = useMemo(
() => ({
progress,
updateProgress,
isStored: rest.isPersistent,
}),
[progress, updateProgress, rest.isPersistent],
);

return (
<ProgressContext.Provider
value={{
progress,
setProgress,
isStored: isPersistent,
}}
>
<ProgressContext.Provider value={contextValue}>
{children}
</ProgressContext.Provider>
);
Expand Down
6 changes: 3 additions & 3 deletions libs/@guardian/react-crossword/src/hooks/useClearUserInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import { useValidAnswers } from '../context/ValidAnswers';
import { getNewProgress } from '../utils/getNewProgress';

export const useClearUserInput = () => {
const { setProgress } = useProgress();
const { updateProgress } = useProgress();
const { dimensions } = useData();
const { setValidAnswers } = useValidAnswers();

const clearUserInput = useCallback(() => {
setProgress(getNewProgress(dimensions));
updateProgress(getNewProgress(dimensions));
setValidAnswers(new Set());
}, [setProgress, setValidAnswers, dimensions]);
}, [updateProgress, setValidAnswers, dimensions]);
return { clearUserInput };
};
68 changes: 0 additions & 68 deletions libs/@guardian/react-crossword/src/hooks/useStoredState.ts

This file was deleted.

25 changes: 11 additions & 14 deletions libs/@guardian/react-crossword/src/hooks/useUpdateCell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useProgress } from '../context/Progress';
import { useValidAnswers } from '../context/ValidAnswers';

export const useUpdateCell = () => {
const { setProgress } = useProgress();
const { updateProgress, progress } = useProgress();
const { setValidAnswers } = useValidAnswers();
const { cells } = useData();

Expand All @@ -19,20 +19,17 @@ export const useUpdateCell = () => {
if (isUndefined(cellGroup)) {
return;
}
const newProgress = [...progress];
if (isUndefined(newProgress[x])) {
throw new Error('Invalid x coordinate');
}

setProgress((prevProgress) => {
const newProgress = [...(prevProgress ?? [])];
if (isUndefined(newProgress[x])) {
throw new Error('Invalid x coordinate');
}

if (isUndefined(newProgress[x][y])) {
throw new Error('Invalid y coordinate');
}
if (isUndefined(newProgress[x][y])) {
throw new Error('Invalid y coordinate');
}

newProgress[x][y] = value;
return newProgress;
});
newProgress[x][y] = value;
updateProgress(newProgress);

setValidAnswers((prev) => {
const newSet = new Set(prev);
Expand All @@ -42,7 +39,7 @@ export const useUpdateCell = () => {
return newSet;
});
},
[cells, setProgress, setValidAnswers],
[cells, progress, updateProgress, setValidAnswers],
);
return { updateCell };
};

0 comments on commit 80a747e

Please sign in to comment.