fix(client): debounce challenge submissions (#67039)
Some checks failed
CD - Docker - GHCR Images / Build and Push Images (push) Has been cancelled
i18n - Build Validation / Validate i18n Builds (24) (push) Has been cancelled
CI - Node.js / Lint (24) (push) Has been cancelled
CD - Docker - DOCR Cleanup Container Images / Delete Old Images (learn-api, dev) (push) Has been cancelled
CD - Docker - DOCR Cleanup Container Images / Delete Old Images (learn-api, org) (push) Has been cancelled
CI - Node.js / Build (24) (push) Has been cancelled
CI - Node.js / Test (24) (push) Has been cancelled
CI - Node.js / Test - Upcoming Changes (24) (push) Has been cancelled
CI - Node.js / Test - i18n (italian, 24) (push) Has been cancelled
CI - Node.js / Test - i18n (portuguese, 24) (push) Has been cancelled
i18n - Download Client UI / Client (push) Has been cancelled

This commit is contained in:
Ayush Kumar Singh 2026-04-24 13:45:17 +05:30 committed by GitHub
parent ed8c673dbb
commit ec06a99fdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 102 additions and 8 deletions

View File

@ -16,7 +16,6 @@ import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import store from 'store';
import { debounce } from 'lodash-es';
import { useTranslation } from 'react-i18next';
import { Loader } from '../../../components/helpers';
import { LocalStorageThemes } from '../../../redux/types';
@ -316,10 +315,6 @@ const Editor = (props: EditorProps): JSX.Element => {
const submitChallenge = useSubmit();
const submitChallengeDebounceRef = useRef(
debounce(submitChallenge, 1000, { leading: true, trailing: false })
);
const player = useRef<{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
sampler: any;
@ -820,7 +815,7 @@ const Editor = (props: EditorProps): JSX.Element => {
props.executeChallenge();
}
const tryToSubmitChallenge = submitChallengeDebounceRef.current;
const tryToSubmitChallenge = submitChallenge;
// TODO: there's a potential performance gain to be had by only updating when
// the outputViewZone has actually changed.

View File

@ -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);
});
});

View File

@ -1,5 +1,6 @@
import { useDispatch } from 'react-redux';
import { useStaticQuery, graphql } from 'gatsby';
import { useEffect, useRef } from 'react';
import { submitChallenge } from '../redux/actions';
import { curriculumData } from '../../../services/curriculum-data';
@ -8,7 +9,8 @@ import type {
ChallengeNode,
SuperBlockStructure
} from '../../../redux/prop-types';
import { useEffect } from 'react';
const SUBMIT_DEBOUNCE_MS = 1000;
interface AllCurriculumData {
allChallengeNode: { nodes: ChallengeNode[] };
@ -87,6 +89,31 @@ export function useSubmit() {
// Ensure curriculum data is loaded before challenge submission
useFetchAllCurriculumData();
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());
};
}