Skip to content

Commit cb25638

Browse files
yungstersfacebook-github-bot
authored andcommitted
RN: Create useRefEffect Utility
Summary: Creates `useRefEffect` which will be used by components in React Native. Changelog: [Internal] Reviewed By: lunaleaps Differential Revision: D28862440 fbshipit-source-id: 50e0099c1a3e0a0f506bf82e68984fc5a032f101
1 parent 0b994ac commit cb25638

File tree

2 files changed

+287
-0
lines changed

2 files changed

+287
-0
lines changed
Lines changed: 242 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,242 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @emails oncall+react_native
8+
* @flow strict-local
9+
* @format
10+
*/
11+
12+
import useRefEffect from '../useRefEffect';
13+
import * as React from 'react';
14+
import {View} from 'react-native';
15+
import {act, create} from 'react-test-renderer';
16+
17+
/**
18+
* TestView provide a component execution environment to test hooks.
19+
*/
20+
function TestView({childKey = null, effect}) {
21+
const ref = useRefEffect(effect);
22+
return <View key={childKey} ref={ref} testID={childKey} />;
23+
}
24+
25+
/**
26+
* TestEffect represents an effect invocation.
27+
*/
28+
class TestEffect {
29+
name: string;
30+
key: ?string;
31+
constructor(name: string, key: ?string) {
32+
this.name = name;
33+
this.key = key;
34+
}
35+
static called(name: string, key: ?string) {
36+
// $FlowIssue[prop-missing] - Flow does not support type augmentation.
37+
return expect.effect(name, key);
38+
}
39+
}
40+
41+
/**
42+
* TestEffectCleanup represents an effect cleanup invocation.
43+
*/
44+
class TestEffectCleanup {
45+
name: string;
46+
key: ?string;
47+
constructor(name: string, key: ?string) {
48+
this.name = name;
49+
this.key = key;
50+
}
51+
static called(name: string, key: ?string) {
52+
// $FlowIssue[prop-missing] - Flow does not support type augmentation.
53+
return expect.effectCleanup(name, key);
54+
}
55+
}
56+
57+
/**
58+
* extend.effect and expect.extendCleanup make it easier to assert expected
59+
* values. But use TestEffect.called and TestEffectCleanup.called instead of
60+
* extend.effect and expect.extendCleanup because of Flow.
61+
*/
62+
expect.extend({
63+
effect(received, name, key) {
64+
const pass =
65+
received instanceof TestEffect &&
66+
received.name === name &&
67+
received.key === key;
68+
return {pass};
69+
},
70+
effectCleanup(received, name, key) {
71+
const pass =
72+
received instanceof TestEffectCleanup &&
73+
received.name === name &&
74+
received.key === key;
75+
return {pass};
76+
},
77+
});
78+
79+
function mockEffectRegistry(): {
80+
mockEffect: string => () => () => void,
81+
mockEffectWithoutCleanup: string => () => void,
82+
registry: $ReadOnlyArray<TestEffect | TestEffectCleanup>,
83+
} {
84+
const registry = [];
85+
return {
86+
mockEffect(name: string): () => () => void {
87+
return instance => {
88+
const key = instance?.props?.testID;
89+
registry.push(new TestEffect(name, key));
90+
return () => {
91+
registry.push(new TestEffectCleanup(name, key));
92+
};
93+
};
94+
},
95+
mockEffectWithoutCleanup(name: string): () => void {
96+
return instance => {
97+
const key = instance?.props?.testID;
98+
registry.push(new TestEffect(name, key));
99+
};
100+
},
101+
registry,
102+
};
103+
}
104+
105+
test('calls effect without cleanup', () => {
106+
let root;
107+
108+
const {mockEffectWithoutCleanup, registry} = mockEffectRegistry();
109+
const effectA = mockEffectWithoutCleanup('A');
110+
111+
act(() => {
112+
root = create(<TestView childKey="foo" effect={effectA} />);
113+
});
114+
115+
expect(registry).toEqual([TestEffect.called('A', 'foo')]);
116+
117+
act(() => {
118+
root.unmount();
119+
});
120+
121+
expect(registry).toEqual([TestEffect.called('A', 'foo')]);
122+
});
123+
124+
test('calls effect and cleanup', () => {
125+
let root;
126+
127+
const {mockEffect, registry} = mockEffectRegistry();
128+
const effectA = mockEffect('A');
129+
130+
act(() => {
131+
root = create(<TestView childKey="foo" effect={effectA} />);
132+
});
133+
134+
expect(registry).toEqual([TestEffect.called('A', 'foo')]);
135+
136+
act(() => {
137+
root.unmount();
138+
});
139+
140+
expect(registry).toEqual([
141+
TestEffect.called('A', 'foo'),
142+
TestEffectCleanup.called('A', 'foo'),
143+
]);
144+
});
145+
146+
test('cleans up old effect before calling new effect', () => {
147+
let root;
148+
149+
const {mockEffect, registry} = mockEffectRegistry();
150+
const effectA = mockEffect('A');
151+
const effectB = mockEffect('B');
152+
153+
act(() => {
154+
root = create(<TestView childKey="foo" effect={effectA} />);
155+
});
156+
157+
act(() => {
158+
root.update(<TestView childKey="foo" effect={effectB} />);
159+
});
160+
161+
expect(registry).toEqual([
162+
TestEffect.called('A', 'foo'),
163+
TestEffectCleanup.called('A', 'foo'),
164+
TestEffect.called('B', 'foo'),
165+
]);
166+
167+
act(() => {
168+
root.unmount();
169+
});
170+
171+
expect(registry).toEqual([
172+
TestEffect.called('A', 'foo'),
173+
TestEffectCleanup.called('A', 'foo'),
174+
TestEffect.called('B', 'foo'),
175+
TestEffectCleanup.called('B', 'foo'),
176+
]);
177+
});
178+
179+
test('calls cleanup and effect on new instance', () => {
180+
let root;
181+
182+
const {mockEffect, registry} = mockEffectRegistry();
183+
const effectA = mockEffect('A');
184+
185+
act(() => {
186+
root = create(<TestView childKey="foo" effect={effectA} />);
187+
});
188+
189+
act(() => {
190+
root.update(<TestView childKey="bar" effect={effectA} />);
191+
});
192+
193+
expect(registry).toEqual([
194+
TestEffect.called('A', 'foo'),
195+
TestEffectCleanup.called('A', 'foo'),
196+
TestEffect.called('A', 'bar'),
197+
]);
198+
199+
act(() => {
200+
root.unmount();
201+
});
202+
203+
expect(registry).toEqual([
204+
TestEffect.called('A', 'foo'),
205+
TestEffectCleanup.called('A', 'foo'),
206+
TestEffect.called('A', 'bar'),
207+
TestEffectCleanup.called('A', 'bar'),
208+
]);
209+
});
210+
211+
test('cleans up old effect before calling new effect with new instance', () => {
212+
let root;
213+
214+
const {mockEffect, registry} = mockEffectRegistry();
215+
const effectA = mockEffect('A');
216+
const effectB = mockEffect('B');
217+
218+
act(() => {
219+
root = create(<TestView childKey="foo" effect={effectA} />);
220+
});
221+
222+
act(() => {
223+
root.update(<TestView childKey="bar" effect={effectB} />);
224+
});
225+
226+
expect(registry).toEqual([
227+
TestEffect.called('A', 'foo'),
228+
TestEffectCleanup.called('A', 'foo'),
229+
TestEffect.called('B', 'bar'),
230+
]);
231+
232+
act(() => {
233+
root.unmount();
234+
});
235+
236+
expect(registry).toEqual([
237+
TestEffect.called('A', 'foo'),
238+
TestEffectCleanup.called('A', 'foo'),
239+
TestEffect.called('B', 'bar'),
240+
TestEffectCleanup.called('B', 'bar'),
241+
]);
242+
});

Libraries/Utilities/useRefEffect.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
* @format
9+
*/
10+
11+
import {useCallback, useRef} from 'react';
12+
13+
type CallbackRef<T> = T => mixed;
14+
15+
/**
16+
* Constructs a callback ref that provides similar semantics as `useEffect`. The
17+
* supplied `effect` callback will be called with non-null component instances.
18+
* The `effect` callback can also optionally return a cleanup function.
19+
*
20+
* When a component is updated or unmounted, the cleanup function is called. The
21+
* `effect` callback will then be called again, if applicable.
22+
*
23+
* When a new `effect` callback is supplied, the previously returned cleanup
24+
* function will be called before the new `effect` callback is called with the
25+
* same instance.
26+
*
27+
* WARNING: The `effect` callback should be stable (e.g. using `useCallback`).
28+
*/
29+
export default function useRefEffect<TInstance>(
30+
effect: TInstance => (() => void) | void,
31+
): CallbackRef<TInstance | null> {
32+
const cleanupRef = useRef<(() => void) | void>(undefined);
33+
return useCallback(
34+
instance => {
35+
if (cleanupRef.current) {
36+
cleanupRef.current();
37+
cleanupRef.current = undefined;
38+
}
39+
if (instance != null) {
40+
cleanupRef.current = effect(instance);
41+
}
42+
},
43+
[effect],
44+
);
45+
}

0 commit comments

Comments
 (0)