Skip to content

Commit 8346010

Browse files
committed
feat(hooks): allow to pass dependency array
Fixes #12
1 parent 3f7bfa2 commit 8346010

File tree

4 files changed

+198
-37
lines changed

4 files changed

+198
-37
lines changed

docs/api/ObserveViewport_connectViewport_useViewport.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ Hook effects allow to trigger side effects on change without updating the compon
134134
| options.deferUpdateUntilIdle | boolean | | Defers to trigger updates until the collector is idle. See [Defer Events](../concepts/defer_events.md) |
135135
| options.priority | `'low'`, `'normal'`, `'high'`, `'highest'` | | Allows to set a priority of the update. See [Defer Events](../concepts/scheduler.md) |
136136
| options.recalculateLayoutBeforeUpdate | function | | Enables a way to calculate layout information for all components as a badge before the effect call. Contains `IViewport`, `IScroll` or `IDimensions` as the first argument, dependent of the used hook. See [recalculateLayoutBeforeUpdate](../concepts/recalculateLayoutBeforeUpdate.md) |
137+
| deps | array | | Array with dependencies. In case a value inside the array changes, this will force an update to the effect function |
137138

138139
### Example
139140

lib/__tests__/ObserveViewport.client.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ describe('ObserveViewport', () => {
5656
cleanup();
5757
(window.addEventListener as jest.Mock).mockRestore();
5858
jest.clearAllTimers();
59+
(window as any).scrollX = 0;
60+
(window as any).scrollY = 0;
5961
});
6062

6163
it('should trigger initial scroll value', () => {

lib/__tests__/hooks.client.test.tsx

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
delete window.requestAnimationFrame;
33
jest.useFakeTimers();
44

5-
import React, { useState } from 'react';
5+
import React, { useState, useRef } from 'react';
66
import { act } from 'react-dom/test-utils';
77
import { render, cleanup, fireEvent } from '@testing-library/react';
8-
import { ViewportProvider, useViewport } from '../index';
8+
import { ViewportProvider, useViewport, useLayoutSnapshot } from '../index';
99

1010
const scrollTo = (x: number, y: number) => {
1111
window.scrollTo(x, y);
@@ -35,6 +35,8 @@ describe('hooks', () => {
3535
afterEach(() => {
3636
cleanup();
3737
jest.clearAllTimers();
38+
(window as any).scrollX = 0;
39+
(window as any).scrollY = 0;
3840
});
3941

4042
describe('useViewport', () => {
@@ -124,4 +126,71 @@ describe('hooks', () => {
124126
expect(getByText('scroll: 0,3000')).toBeDefined();
125127
});
126128
});
129+
130+
describe('useLayoutSnapshot', () => {
131+
it('should update snapshot on scroll', () => {
132+
const App = () => {
133+
const ref = useRef<HTMLDivElement>(null);
134+
const snapshot = useLayoutSnapshot(({ scroll }) => {
135+
if (ref.current) {
136+
return `${ref.current.dataset.info},${scroll.y}`;
137+
}
138+
return null;
139+
});
140+
return (
141+
<div ref={ref} data-info="pony">
142+
{snapshot}
143+
</div>
144+
);
145+
};
146+
const { getByText } = render(
147+
<ViewportProvider>
148+
<App />
149+
</ViewportProvider>,
150+
);
151+
scrollTo(0, 1000);
152+
act(() => {
153+
jest.advanceTimersByTime(20);
154+
});
155+
expect(getByText('pony,1000')).toBeDefined();
156+
});
157+
158+
it('should update snapshot on dependency change', () => {
159+
const App = ({ info }: { info: string }) => {
160+
const ref = useRef<HTMLDivElement>(null);
161+
const snapshot = useLayoutSnapshot(
162+
({ scroll }) => {
163+
if (ref.current) {
164+
return `${ref.current.dataset.info},${scroll.y}`;
165+
}
166+
return null;
167+
},
168+
[info],
169+
);
170+
return (
171+
<div ref={ref} data-info={info}>
172+
{snapshot}
173+
</div>
174+
);
175+
};
176+
const { getByText, rerender } = render(
177+
<ViewportProvider>
178+
<App info="pony" />
179+
</ViewportProvider>,
180+
);
181+
act(() => {
182+
jest.advanceTimersByTime(20);
183+
});
184+
expect(getByText('pony,0')).toBeDefined();
185+
rerender(
186+
<ViewportProvider>
187+
<App info="foo" />
188+
</ViewportProvider>,
189+
);
190+
act(() => {
191+
jest.advanceTimersByTime(20);
192+
});
193+
expect(getByText('foo,0')).toBeDefined();
194+
});
195+
});
127196
});

lib/hooks.ts

Lines changed: 124 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { useContext, useEffect, useState, RefObject, useRef } from 'react';
1+
import {
2+
useContext,
3+
useEffect,
4+
useState,
5+
RefObject,
6+
useRef,
7+
DependencyList,
8+
} from 'react';
29

310
import { ViewportContext } from './ViewportProvider';
411
import { IViewport, IScroll, IDimensions, PriorityType, IRect } from './types';
@@ -22,25 +29,28 @@ interface IEffectOptions<T> extends IOptions {
2229
recalculateLayoutBeforeUpdate?: (viewport: IViewport) => T;
2330
}
2431

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);
32+
export function useViewportEffect<T = unknown>(
33+
handleViewportChange: (viewport: IViewport, snapshot: T) => void,
34+
deps?: DependencyList,
35+
): void;
3136

32-
return optionsRef.current;
33-
};
37+
export function useViewportEffect<T = unknown>(
38+
handleViewportChange: (viewport: IViewport, snapshot: T) => void,
39+
options?: IViewPortEffectOptions<T>,
40+
deps?: DependencyList,
41+
): void;
3442

35-
export const useViewportEffect = <T>(
43+
export function useViewportEffect<T>(
3644
handleViewportChange: (viewport: IViewport, snapshot: T) => void,
37-
options: IViewPortEffectOptions<T> = {},
38-
) => {
45+
second?: any,
46+
third?: any,
47+
) {
3948
const {
4049
addViewportChangeListener,
4150
removeViewportChangeListener,
4251
hasRootProviderAsParent,
4352
} = useContext(ViewportContext);
53+
const { options, deps } = sortArgs(second, third);
4454
const memoOptions = useOptions(options);
4555

4656
useEffect(() => {
@@ -56,8 +66,12 @@ export const useViewportEffect = <T>(
5666
recalculateLayoutBeforeUpdate: memoOptions.recalculateLayoutBeforeUpdate,
5767
});
5868
return () => removeViewportChangeListener(handleViewportChange);
59-
}, [addViewportChangeListener || null, removeViewportChangeListener || null]);
60-
};
69+
}, [
70+
addViewportChangeListener || null,
71+
removeViewportChangeListener || null,
72+
...deps,
73+
]);
74+
}
6175

6276
export const useViewport = (options: IFullOptions = {}): IViewport => {
6377
const { getCurrentViewport } = useContext(ViewportContext);
@@ -67,18 +81,32 @@ export const useViewport = (options: IFullOptions = {}): IViewport => {
6781
return state;
6882
};
6983

70-
export const useScrollEffect = <T = unknown>(
84+
export function useScrollEffect<T = unknown>(
7185
effect: (scroll: IScroll, snapshot: T) => void,
72-
options: IEffectOptions<T> = {},
73-
) => {
86+
deps?: DependencyList,
87+
): void;
88+
89+
export function useScrollEffect<T = unknown>(
90+
effect: (scroll: IScroll, snapshot: T) => void,
91+
options: IEffectOptions<T>,
92+
deps?: DependencyList,
93+
): void;
94+
95+
export function useScrollEffect<T = unknown>(
96+
effect: (scroll: IScroll, snapshot: T) => void,
97+
second?: any,
98+
third?: any,
99+
) {
100+
const { options, deps } = sortArgs(second, third);
74101
useViewportEffect(
75102
(viewport, snapshot: T) => effect(viewport.scroll, snapshot),
76103
{
77104
disableDimensionsUpdates: true,
78105
...options,
79106
},
107+
deps,
80108
);
81-
};
109+
}
82110

83111
export const useScroll = (options: IOptions = {}): IScroll => {
84112
const { scroll } = useViewport({
@@ -89,18 +117,32 @@ export const useScroll = (options: IOptions = {}): IScroll => {
89117
return scroll;
90118
};
91119

92-
export const useDimensionsEffect = <T = unknown>(
93-
effect: (scroll: IDimensions, snapshot: T) => void,
94-
options: IEffectOptions<T> = {},
95-
) => {
120+
export function useDimensionsEffect<T = unknown>(
121+
effect: (dimensions: IDimensions, snapshot: T) => void,
122+
deps?: DependencyList,
123+
): void;
124+
125+
export function useDimensionsEffect<T = unknown>(
126+
effect: (dimensions: IDimensions, snapshot: T) => void,
127+
options: IEffectOptions<T>,
128+
deps?: DependencyList,
129+
): void;
130+
131+
export function useDimensionsEffect<T = unknown>(
132+
effect: (dimensions: IDimensions, snapshot: T) => void,
133+
second: any,
134+
third?: any,
135+
) {
136+
const { options, deps } = sortArgs(second, third);
96137
useViewportEffect(
97138
(viewport, snapshot: T) => effect(viewport.dimensions, snapshot),
98139
{
99140
disableScrollUpdates: true,
100141
...options,
101142
},
143+
deps,
102144
);
103-
};
145+
}
104146

105147
export const useDimensions = (options: IOptions = {}): IDimensions => {
106148
const { dimensions } = useViewport({
@@ -116,11 +158,15 @@ export const useRectEffect = (
116158
ref: RefObject<HTMLElement>,
117159
options?: IFullOptions,
118160
) => {
119-
useViewportEffect((_, snapshot) => effect(snapshot), {
120-
...options,
121-
recalculateLayoutBeforeUpdate: () =>
122-
ref.current ? ref.current.getBoundingClientRect() : null,
123-
});
161+
useViewportEffect(
162+
(_, snapshot) => effect(snapshot),
163+
{
164+
...options,
165+
recalculateLayoutBeforeUpdate: () =>
166+
ref.current ? ref.current.getBoundingClientRect() : null,
167+
},
168+
[ref.current],
169+
);
124170
};
125171

126172
export const useRect = (
@@ -130,18 +176,61 @@ export const useRect = (
130176
return useLayoutSnapshot(
131177
() => (ref.current ? ref.current.getBoundingClientRect() : null),
132178
options,
179+
[ref.current],
133180
);
134181
};
135182

136-
export const useLayoutSnapshot = <T = unknown>(
183+
export function useLayoutSnapshot<T = unknown>(
184+
recalculateLayoutBeforeUpdate: (viewport: IViewport) => T,
185+
deps?: DependencyList,
186+
): null | T;
187+
188+
export function useLayoutSnapshot<T = unknown>(
137189
recalculateLayoutBeforeUpdate: (viewport: IViewport) => T,
138-
options: IFullOptions = {},
139-
): null | T => {
190+
options?: IFullOptions,
191+
deps?: DependencyList,
192+
): null | T;
193+
194+
export function useLayoutSnapshot<T = unknown>(
195+
recalculateLayoutBeforeUpdate: (viewport: IViewport) => T,
196+
second?: any,
197+
third?: any,
198+
): null | T {
199+
const { options, deps } = sortArgs(second, third);
140200
const [state, setSnapshot] = useState<null | T>(null);
141-
useViewportEffect((_, snapshot: T) => setSnapshot(snapshot), {
142-
...options,
143-
recalculateLayoutBeforeUpdate,
144-
});
201+
useViewportEffect(
202+
(_, snapshot: T) => setSnapshot(snapshot),
203+
{
204+
...options,
205+
recalculateLayoutBeforeUpdate,
206+
},
207+
deps,
208+
);
145209

146210
return state;
211+
}
212+
213+
const useOptions = <T>(o: IViewPortEffectOptions<T>) => {
214+
const optionsRef = useRef<IViewPortEffectOptions<T>>(Object.create(null));
215+
for (const key of Object.keys(optionsRef.current)) {
216+
delete optionsRef.current[key];
217+
}
218+
Object.assign(optionsRef.current, o);
219+
220+
return optionsRef.current;
221+
};
222+
223+
const sortArgs = (
224+
first: DependencyList | IOptions,
225+
second?: DependencyList,
226+
) => {
227+
let options = {};
228+
if (first && !Array.isArray(first)) {
229+
options = first;
230+
}
231+
let deps = second || [];
232+
if (first && Array.isArray(first)) {
233+
deps = first;
234+
}
235+
return { deps, options };
147236
};

0 commit comments

Comments
 (0)