feat: integrate deferred loading for saving state in DotEnvFileEditor (#7463)

This commit is contained in:
naman-bruno
2026-03-13 17:14:23 +05:30
committed by GitHub
parent a8542c7312
commit ab18a6ba84
3 changed files with 152 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ import { uuid } from 'utils/common';
import { useFormik } from 'formik';
import { variableNameRegex } from 'utils/common/regex';
import toast from 'react-hot-toast';
import useDeferredLoading from 'hooks/useDeferredLoading';
import StyledWrapper from './StyledWrapper';
import DotEnvTableView from './DotEnvTableView';
@@ -31,6 +32,7 @@ const DotEnvFileEditor = ({
const [rawValue, setRawValue] = useState(initialRawValue);
const [prevViewMode, setPrevViewMode] = useState(viewMode);
const [isSaving, setIsSaving] = useState(false);
const showSaving = useDeferredLoading(isSaving, 200);
const formikRef = useRef(null);
@@ -311,7 +313,7 @@ const DotEnvFileEditor = ({
onChange={handleRawChange}
onSave={handleSaveRaw}
onReset={handleReset}
isSaving={isSaving}
isSaving={showSaving}
/>
</StyledWrapper>
);
@@ -335,7 +337,7 @@ const DotEnvFileEditor = ({
onRemoveVar={handleRemoveVar}
onSave={handleSave}
onReset={handleReset}
isSaving={isSaving}
isSaving={showSaving}
/>
</StyledWrapper>
);

View File

@@ -0,0 +1,39 @@
import { useState, useEffect, useRef } from 'react';
/**
* A hook that defers showing loading state until a minimum delay has passed.
* This prevents flickering UI for fast operations.
*
* @param {boolean} isLoading - The actual loading state
* @param {number} delay - Minimum time (ms) before showing loading state (default: 200ms)
* @returns {boolean} - The deferred loading state
*/
function useDeferredLoading(isLoading, delay = 200) {
const [showLoading, setShowLoading] = useState(false);
const timerRef = useRef(null);
useEffect(() => {
if (isLoading) {
timerRef.current = setTimeout(() => {
setShowLoading(true);
}, delay);
} else {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
setShowLoading(false);
}
return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
};
}, [isLoading, delay]);
return showLoading;
}
export default useDeferredLoading;

View File

@@ -0,0 +1,109 @@
const { describe, it, expect, beforeEach, afterEach } = require('@jest/globals');
import { renderHook, act } from '@testing-library/react';
import useDeferredLoading from './index';
describe('useDeferredLoading', () => {
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.useRealTimers();
});
it('should return false initially when isLoading is false', () => {
const { result } = renderHook(() => useDeferredLoading(false));
expect(result.current).toBe(false);
});
it('should not show loading immediately when isLoading becomes true', () => {
const { result } = renderHook(() => useDeferredLoading(true, 200));
expect(result.current).toBe(false);
});
it('should show loading after the delay has passed', () => {
const { result } = renderHook(() => useDeferredLoading(true, 200));
expect(result.current).toBe(false);
act(() => {
jest.advanceTimersByTime(200);
});
expect(result.current).toBe(true);
});
it('should not show loading if isLoading becomes false before delay', () => {
const { result, rerender } = renderHook(
({ isLoading }) => useDeferredLoading(isLoading, 200),
{ initialProps: { isLoading: true } }
);
expect(result.current).toBe(false);
act(() => {
jest.advanceTimersByTime(100);
});
expect(result.current).toBe(false);
rerender({ isLoading: false });
act(() => {
jest.advanceTimersByTime(200);
});
expect(result.current).toBe(false);
});
it('should reset to false immediately when isLoading becomes false', () => {
const { result, rerender } = renderHook(
({ isLoading }) => useDeferredLoading(isLoading, 200),
{ initialProps: { isLoading: true } }
);
act(() => {
jest.advanceTimersByTime(200);
});
expect(result.current).toBe(true);
rerender({ isLoading: false });
expect(result.current).toBe(false);
});
it('should use default delay of 200ms', () => {
const { result } = renderHook(() => useDeferredLoading(true));
expect(result.current).toBe(false);
act(() => {
jest.advanceTimersByTime(199);
});
expect(result.current).toBe(false);
act(() => {
jest.advanceTimersByTime(1);
});
expect(result.current).toBe(true);
});
it('should respect custom delay values', () => {
const { result } = renderHook(() => useDeferredLoading(true, 500));
act(() => {
jest.advanceTimersByTime(400);
});
expect(result.current).toBe(false);
act(() => {
jest.advanceTimersByTime(100);
});
expect(result.current).toBe(true);
});
});