Skip to content

Commit f3af6a0

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 2cf7efb commit f3af6a0

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,
@@ -12,7 +13,7 @@ import type {
1213
TokensResult,
1314
} from '../src/lib/types';
1415
import type { ShikiTransformer } from 'shiki';
15-
import { throttleHighlighting } from '../src/lib/utils';
16+
import { throttleHighlighting, useStableOptions } from '../src/lib/utils';
1617

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

0 commit comments

Comments
 (0)