Skip to content

Commit 3f7bfa2

Browse files
committed
fix(hooks): allow updates to options at runtime
1 parent 94b6c31 commit 3f7bfa2

File tree

3 files changed

+145
-8
lines changed

3 files changed

+145
-8
lines changed

lib/__tests__/hooks.client.test.tsx

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// force fallback to setTimeout
2+
delete window.requestAnimationFrame;
3+
jest.useFakeTimers();
4+
5+
import React, { useState } from 'react';
6+
import { act } from 'react-dom/test-utils';
7+
import { render, cleanup, fireEvent } from '@testing-library/react';
8+
import { ViewportProvider, useViewport } from '../index';
9+
10+
const scrollTo = (x: number, y: number) => {
11+
window.scrollTo(x, y);
12+
act(() => {
13+
jest.advanceTimersByTime(20);
14+
});
15+
};
16+
17+
describe('hooks', () => {
18+
beforeEach(() => {
19+
const eventMap: any = {
20+
scroll: jest.fn(),
21+
};
22+
jest.spyOn(window, 'addEventListener').mockImplementation((event, cb) => {
23+
eventMap[event] = cb;
24+
});
25+
26+
jest
27+
.spyOn(window, 'scrollTo')
28+
.mockImplementation((x: number, y: number) => {
29+
(window as any).scrollX = x;
30+
(window as any).scrollY = y;
31+
eventMap.scroll && eventMap.scroll();
32+
});
33+
});
34+
35+
afterEach(() => {
36+
cleanup();
37+
jest.clearAllTimers();
38+
});
39+
40+
describe('useViewport', () => {
41+
it('should update on viewport change', async () => {
42+
const App = () => {
43+
const viewport = useViewport();
44+
return (
45+
<div>
46+
scroll: {viewport.scroll.x},{viewport.scroll.y}
47+
</div>
48+
);
49+
};
50+
const { getByText } = render(
51+
<ViewportProvider>
52+
<App />
53+
</ViewportProvider>,
54+
);
55+
act(() => {
56+
jest.advanceTimersByTime(20);
57+
});
58+
scrollTo(0, 1000);
59+
expect(getByText('scroll: 0,1000')).toBeDefined();
60+
61+
scrollTo(0, 2000);
62+
expect(getByText('scroll: 0,2000')).toBeDefined();
63+
});
64+
65+
it('should not update if disabled', async () => {
66+
const App = () => {
67+
const viewport = useViewport({
68+
disableScrollUpdates: true,
69+
});
70+
return (
71+
<div>
72+
scroll: {viewport.scroll.x},{viewport.scroll.y}
73+
</div>
74+
);
75+
};
76+
const { rerender, getByText } = render(<ViewportProvider />);
77+
scrollTo(0, 1000);
78+
rerender(
79+
<ViewportProvider>
80+
<App />
81+
</ViewportProvider>,
82+
);
83+
act(() => {
84+
jest.advanceTimersByTime(20);
85+
});
86+
expect(getByText('scroll: 0,1000')).toBeDefined();
87+
scrollTo(0, 2000);
88+
expect(getByText('scroll: 0,1000')).toBeDefined();
89+
});
90+
91+
it('should not update if disabled at runtime', async () => {
92+
const App = () => {
93+
const [disableScrollUpdates, setDisableScrollUpdate] = useState(false);
94+
const viewport = useViewport({
95+
disableScrollUpdates,
96+
});
97+
return (
98+
<div onClick={() => setDisableScrollUpdate(!disableScrollUpdates)}>
99+
scroll: {viewport.scroll.x},{viewport.scroll.y}
100+
</div>
101+
);
102+
};
103+
const { rerender, getByText } = render(<ViewportProvider />);
104+
scrollTo(0, 1000);
105+
rerender(
106+
<ViewportProvider>
107+
<App />
108+
</ViewportProvider>,
109+
);
110+
act(() => {
111+
jest.advanceTimersByTime(20);
112+
});
113+
expect(getByText('scroll: 0,1000')).toBeDefined();
114+
115+
// disable
116+
fireEvent.click(getByText('scroll: 0,1000'));
117+
scrollTo(0, 2000);
118+
expect(getByText('scroll: 0,1000')).toBeDefined();
119+
120+
// enable
121+
fireEvent.click(getByText('scroll: 0,1000'));
122+
123+
scrollTo(0, 3000);
124+
expect(getByText('scroll: 0,3000')).toBeDefined();
125+
});
126+
});
127+
});

lib/hooks.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext, useEffect, useState, RefObject } from 'react';
1+
import { useContext, useEffect, useState, RefObject, useRef } from 'react';
22

33
import { ViewportContext } from './ViewportProvider';
44
import { IViewport, IScroll, IDimensions, PriorityType, IRect } from './types';
@@ -14,13 +14,24 @@ interface IFullOptions extends IOptions {
1414
}
1515

1616
interface IOptions {
17+
[key: string]: unknown;
1718
deferUpdateUntilIdle?: boolean;
1819
priority?: PriorityType;
1920
}
2021
interface IEffectOptions<T> extends IOptions {
2122
recalculateLayoutBeforeUpdate?: (viewport: IViewport) => T;
2223
}
2324

25+
const useOptions = <T>(o: IViewPortEffectOptions<T>) => {
26+
const optionsRef = useRef<IViewPortEffectOptions<T>>(Object.create(null));
27+
for (const key of Object.keys(optionsRef.current)) {
28+
delete optionsRef.current[key];
29+
}
30+
Object.assign(optionsRef.current, o);
31+
32+
return optionsRef.current;
33+
};
34+
2435
export const useViewportEffect = <T>(
2536
handleViewportChange: (viewport: IViewport, snapshot: T) => void,
2637
options: IViewPortEffectOptions<T> = {},
@@ -30,18 +41,19 @@ export const useViewportEffect = <T>(
3041
removeViewportChangeListener,
3142
hasRootProviderAsParent,
3243
} = useContext(ViewportContext);
44+
const memoOptions = useOptions(options);
3345

3446
useEffect(() => {
3547
if (!hasRootProviderAsParent) {
3648
warnNoContextAvailable('useViewport');
3749
return;
3850
}
3951
addViewportChangeListener(handleViewportChange, {
40-
notifyScroll: () => !options.disableScrollUpdates,
41-
notifyDimensions: () => !options.disableDimensionsUpdates,
42-
notifyOnlyWhenIdle: () => Boolean(options.deferUpdateUntilIdle),
43-
priority: () => options.priority || 'normal',
44-
recalculateLayoutBeforeUpdate: options.recalculateLayoutBeforeUpdate,
52+
notifyScroll: () => !memoOptions.disableScrollUpdates,
53+
notifyDimensions: () => !memoOptions.disableDimensionsUpdates,
54+
notifyOnlyWhenIdle: () => Boolean(memoOptions.deferUpdateUntilIdle),
55+
priority: () => memoOptions.priority || 'normal',
56+
recalculateLayoutBeforeUpdate: memoOptions.recalculateLayoutBeforeUpdate,
4557
});
4658
return () => removeViewportChangeListener(handleViewportChange);
4759
}, [addViewportChangeListener || null, removeViewportChangeListener || null]);

lib/modules.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,5 +65,3 @@ interface DOMRectReadOnly {
6565

6666
toJSON: () => any;
6767
}
68-
69-
declare module '@testing-library/react';

0 commit comments

Comments
 (0)