I am building quiz app and there is a problem that i have to shuffle question options but they are located in the nested question array. Here is the interface:
export interface IConnectionsQuestionData extends IQuestionData {
type: 'connections';
questionsTitle: string;
answersTitle: string;
pairs: {
questionOption: IConnectionsOption;
answerOption: Maybe<IConnectionsOption>;
}[];
}
export interface IConnectionsOption {
key: IDType;
value: string;
type: 'answer' | 'question'
}
So in order to shuffle options i have created custom useShuffle hook:
export const useShuffle = <T>(array: T[]): T[] => {
return useMemo(() => {
return shuffleArray(array);
}, [array])
}
In Question component I get question from props. I use this hook like this:
const shuffledLeftSideOptions = useShuffle(question?.pairs.map(pair => pair.questionOption) ?? [])
const shuffledRightSideOptions = useShuffle(question?.pairs.map(pair => pair.answerOption) ?? [])
But every time component need to be rerendered when i choose the option, options array shuffles again on every render. I tried to test it and it works fine with pairs array but reshuffles with question or answer options.
>Solution :
You’ve only memoized the shuffling, not the map operation. So you get a new array every time, because map produces a new array every time, and so useShuffle has to do its work every time.
If you need to do the map, you’ll need to memoize that as well, e.g.:
const mappedLeftOptions = useMemo(
() => question?.pairs.map((pair) => pair.questionOption) ?? [],
);
const mappedRightOptions = useMemo(
() => question?.pairs.map((pair) => pair.answerOption) ?? [],
);
const shuffledLeftSideOptions = useShuffle(mappedLeftOptions);
const shuffledRightSideOptions = useShuffle(mappedRightOptions);
But, beware that the memoization provided by useMemo provides only a performance optimization, not a semantic guarantee. (More in the documentation.) But your shuffling code is relying on it as though it were a semantic guarantee.
Instead, when you need a semantic guarantee, use a ref:
export const useShuffle = <T>(array: T[]): T[] => {
const arrayRef = useRef<T[] | null>(null);
const shuffleRef = useRef<T[] | null>(null);
if (!Object.is(array, arrayRef.current)) {
shuffleRef.current = null;
}
if (!shuffleRef.current) {
arrayRef.current = array;
shuffleRef.current = shuffleArray(array);
}
return shuffleRef.current;
};
Or it’s probably cleaner with a single ref:
export const useShuffle = <T>(array: T[]): T[] => {
const ref = useRef<{ array: T[]; shuffled: T[] } | null>(null);
if (!ref.current || !Object.is(ref.current.array, array)) {
ref.current = {
array,
shuffled: shuffleArray(array),
};
}
return ref.current.shuffled;
};