mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(client): debounce challenge submissions (#67039)
This commit is contained in:
committed by
GitHub
parent
ed8c673dbb
commit
ec06a99fdb
@@ -16,7 +16,6 @@ import { connect } from 'react-redux';
|
|||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
|
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Loader } from '../../../components/helpers';
|
import { Loader } from '../../../components/helpers';
|
||||||
import { LocalStorageThemes } from '../../../redux/types';
|
import { LocalStorageThemes } from '../../../redux/types';
|
||||||
@@ -316,10 +315,6 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
|
|
||||||
const submitChallenge = useSubmit();
|
const submitChallenge = useSubmit();
|
||||||
|
|
||||||
const submitChallengeDebounceRef = useRef(
|
|
||||||
debounce(submitChallenge, 1000, { leading: true, trailing: false })
|
|
||||||
);
|
|
||||||
|
|
||||||
const player = useRef<{
|
const player = useRef<{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
sampler: any;
|
sampler: any;
|
||||||
@@ -820,7 +815,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
|||||||
props.executeChallenge();
|
props.executeChallenge();
|
||||||
}
|
}
|
||||||
|
|
||||||
const tryToSubmitChallenge = submitChallengeDebounceRef.current;
|
const tryToSubmitChallenge = submitChallenge;
|
||||||
|
|
||||||
// TODO: there's a potential performance gain to be had by only updating when
|
// TODO: there's a potential performance gain to be had by only updating when
|
||||||
// the outputViewZone has actually changed.
|
// the outputViewZone has actually changed.
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { renderHook, act } from '@testing-library/react';
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { useSubmit } from './fetch-all-curriculum-data';
|
||||||
|
|
||||||
|
const { mockDispatch } = vi.hoisted(() => ({
|
||||||
|
mockDispatch: vi.fn()
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('react-redux', () => ({
|
||||||
|
useDispatch: () => mockDispatch
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock('gatsby', () => ({
|
||||||
|
graphql: vi.fn(),
|
||||||
|
useStaticQuery: () => ({
|
||||||
|
allChallengeNode: { nodes: [] },
|
||||||
|
allCertificateNode: { nodes: [] },
|
||||||
|
allSuperBlockStructure: { nodes: [] }
|
||||||
|
})
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('useSubmit', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
mockDispatch.mockReset();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should debounce rapid submissions', () => {
|
||||||
|
const { result } = renderHook(() => useSubmit());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current();
|
||||||
|
result.current();
|
||||||
|
result.current();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(1001);
|
||||||
|
result.current();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should debounce per hook instance', () => {
|
||||||
|
const { result: first } = renderHook(() => useSubmit());
|
||||||
|
const { result: second } = renderHook(() => useSubmit());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
first.current();
|
||||||
|
second.current();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
vi.advanceTimersByTime(1001);
|
||||||
|
first.current();
|
||||||
|
second.current();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(mockDispatch).toHaveBeenCalledTimes(4);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { useStaticQuery, graphql } from 'gatsby';
|
import { useStaticQuery, graphql } from 'gatsby';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { submitChallenge } from '../redux/actions';
|
import { submitChallenge } from '../redux/actions';
|
||||||
import { curriculumData } from '../../../services/curriculum-data';
|
import { curriculumData } from '../../../services/curriculum-data';
|
||||||
@@ -8,7 +9,8 @@ import type {
|
|||||||
ChallengeNode,
|
ChallengeNode,
|
||||||
SuperBlockStructure
|
SuperBlockStructure
|
||||||
} from '../../../redux/prop-types';
|
} from '../../../redux/prop-types';
|
||||||
import { useEffect } from 'react';
|
|
||||||
|
const SUBMIT_DEBOUNCE_MS = 1000;
|
||||||
|
|
||||||
interface AllCurriculumData {
|
interface AllCurriculumData {
|
||||||
allChallengeNode: { nodes: ChallengeNode[] };
|
allChallengeNode: { nodes: ChallengeNode[] };
|
||||||
@@ -87,6 +89,31 @@ export function useSubmit() {
|
|||||||
// Ensure curriculum data is loaded before challenge submission
|
// Ensure curriculum data is loaded before challenge submission
|
||||||
useFetchAllCurriculumData();
|
useFetchAllCurriculumData();
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
const isSubmitLockedRef = useRef(false);
|
||||||
|
const submitLockTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
return () => dispatch(submitChallenge());
|
useEffect(
|
||||||
|
() => () => {
|
||||||
|
if (submitLockTimeoutRef.current !== null) {
|
||||||
|
clearTimeout(submitLockTimeoutRef.current);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (isSubmitLockedRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isSubmitLockedRef.current = true;
|
||||||
|
submitLockTimeoutRef.current = setTimeout(() => {
|
||||||
|
isSubmitLockedRef.current = false;
|
||||||
|
submitLockTimeoutRef.current = null;
|
||||||
|
}, SUBMIT_DEBOUNCE_MS);
|
||||||
|
|
||||||
|
return dispatch(submitChallenge());
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user