Skip to content

Commit 3998e93

Browse files
AVGVSTVS96claude
andcommitted
test: add stable options tests for re-render prevention
Verifies useStableOptions returns same reference for identical content, preventing unnecessary re-highlights when options objects are recreated. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 99ed0d3 commit 3998e93

File tree

2 files changed

+107
-6
lines changed

2 files changed

+107
-6
lines changed

package/src/lib/utils.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,8 @@ export const useStableOptions = <T>(value: T): T => {
1717
return value;
1818
}
1919

20-
if (value !== ref.current) {
21-
if (!dequal(value, ref.current)) {
22-
ref.current = value;
23-
revision.current += 1;
24-
}
20+
if (value !== ref.current && !dequal(value, ref.current)) {
21+
ref.current = value;
2522
}
2623

2724
return ref.current;

package/tests/hook.test.tsx

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { render, renderHook, waitFor } from '@testing-library/react';
2+
import { useRef } from 'react';
23
import { vi } from 'vitest';
34
import {
45
useShikiHighlighter,
@@ -13,7 +14,7 @@ import type {
1314
TokensResult,
1415
} from '../src/lib/types';
1516
import type { ShikiTransformer } from 'shiki';
16-
import { throttleHighlighting } from '../src/lib/utils';
17+
import { throttleHighlighting, useStableOptions } from '../src/lib/utils';
1718

1819
interface TestComponentProps {
1920
code: string;
@@ -949,4 +950,107 @@ describe('useShikiHighlighter Hook', () => {
949950
Date.now = originalDateNow;
950951
});
951952
});
953+
954+
describe('Stable Options', () => {
955+
test('returns same reference for identical object content', () => {
956+
const { result, rerender } = renderHook(
957+
({ options }) => useStableOptions(options),
958+
{ initialProps: { options: { delay: 100 } } }
959+
);
960+
961+
const firstRef = result.current;
962+
963+
// Rerender with new object, same content
964+
rerender({ options: { delay: 100 } });
965+
expect(result.current).toBe(firstRef);
966+
967+
// Rerender with different content
968+
rerender({ options: { delay: 200 } });
969+
expect(result.current).not.toBe(firstRef);
970+
expect(result.current).toEqual({ delay: 200 });
971+
});
972+
973+
test('returns same reference for identical nested objects', () => {
974+
const { result, rerender } = renderHook(
975+
({ options }) => useStableOptions(options),
976+
{
977+
initialProps: {
978+
options: {
979+
themes: { light: 'github-light', dark: 'github-dark' },
980+
},
981+
},
982+
}
983+
);
984+
985+
const firstRef = result.current;
986+
987+
// Rerender with new object, same nested content
988+
rerender({
989+
options: {
990+
themes: { light: 'github-light', dark: 'github-dark' },
991+
},
992+
});
993+
expect(result.current).toBe(firstRef);
994+
});
995+
996+
test('handles primitive values correctly', () => {
997+
const { result, rerender } = renderHook(
998+
({ value }) => useStableOptions(value),
999+
{ initialProps: { value: 'typescript' } }
1000+
);
1001+
1002+
expect(result.current).toBe('typescript');
1003+
1004+
rerender({ value: 'typescript' });
1005+
expect(result.current).toBe('typescript');
1006+
1007+
rerender({ value: 'javascript' });
1008+
expect(result.current).toBe('javascript');
1009+
});
1010+
1011+
test('hook does not re-highlight when options object is recreated with same content', async () => {
1012+
let highlightCount = 0;
1013+
1014+
const CountingComponent = ({ options }: { options: object }) => {
1015+
const highlighted = useShikiHighlighter(
1016+
'const x = 1;',
1017+
'javascript',
1018+
'github-dark',
1019+
options
1020+
);
1021+
1022+
// Count when we get a new result (indicates highlight ran)
1023+
const prevRef = useRef(highlighted);
1024+
if (highlighted !== null && highlighted !== prevRef.current) {
1025+
highlightCount++;
1026+
prevRef.current = highlighted;
1027+
}
1028+
1029+
return <div>{highlighted}</div>;
1030+
};
1031+
1032+
const { rerender } = render(
1033+
<CountingComponent options={{ delay: undefined }} />
1034+
);
1035+
1036+
// Wait for initial highlight
1037+
await waitFor(() => {
1038+
expect(highlightCount).toBe(1);
1039+
});
1040+
1041+
// Rerender with new object, same content - should NOT re-highlight
1042+
rerender(<CountingComponent options={{ delay: undefined }} />);
1043+
1044+
// Small wait to ensure no additional highlights triggered
1045+
await new Promise((r) => setTimeout(r, 50));
1046+
expect(highlightCount).toBe(1);
1047+
1048+
// Rerender with different content - SHOULD re-highlight
1049+
rerender(<CountingComponent options={{ showLineNumbers: true }} />);
1050+
1051+
await waitFor(() => {
1052+
expect(highlightCount).toBe(2);
1053+
});
1054+
});
1055+
});
9521056
});

0 commit comments

Comments
 (0)