forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Summary: Creates `useRefEffect` which will be used by components in React Native. Changelog: [Internal] Reviewed By: lunaleaps Differential Revision: D28862440 fbshipit-source-id: 50e0099c1a3e0a0f506bf82e68984fc5a032f101
- Loading branch information
1 parent
0b994ac
commit cb25638
Showing
2 changed files
with
287 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,242 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @emails oncall+react_native | ||
* @flow strict-local | ||
* @format | ||
*/ | ||
|
||
import useRefEffect from '../useRefEffect'; | ||
import * as React from 'react'; | ||
import {View} from 'react-native'; | ||
import {act, create} from 'react-test-renderer'; | ||
|
||
/** | ||
* TestView provide a component execution environment to test hooks. | ||
*/ | ||
function TestView({childKey = null, effect}) { | ||
const ref = useRefEffect(effect); | ||
return <View key={childKey} ref={ref} testID={childKey} />; | ||
} | ||
|
||
/** | ||
* TestEffect represents an effect invocation. | ||
*/ | ||
class TestEffect { | ||
name: string; | ||
key: ?string; | ||
constructor(name: string, key: ?string) { | ||
this.name = name; | ||
this.key = key; | ||
} | ||
static called(name: string, key: ?string) { | ||
// $FlowIssue[prop-missing] - Flow does not support type augmentation. | ||
return expect.effect(name, key); | ||
} | ||
} | ||
|
||
/** | ||
* TestEffectCleanup represents an effect cleanup invocation. | ||
*/ | ||
class TestEffectCleanup { | ||
name: string; | ||
key: ?string; | ||
constructor(name: string, key: ?string) { | ||
this.name = name; | ||
this.key = key; | ||
} | ||
static called(name: string, key: ?string) { | ||
// $FlowIssue[prop-missing] - Flow does not support type augmentation. | ||
return expect.effectCleanup(name, key); | ||
} | ||
} | ||
|
||
/** | ||
* extend.effect and expect.extendCleanup make it easier to assert expected | ||
* values. But use TestEffect.called and TestEffectCleanup.called instead of | ||
* extend.effect and expect.extendCleanup because of Flow. | ||
*/ | ||
expect.extend({ | ||
effect(received, name, key) { | ||
const pass = | ||
received instanceof TestEffect && | ||
received.name === name && | ||
received.key === key; | ||
return {pass}; | ||
}, | ||
effectCleanup(received, name, key) { | ||
const pass = | ||
received instanceof TestEffectCleanup && | ||
received.name === name && | ||
received.key === key; | ||
return {pass}; | ||
}, | ||
}); | ||
|
||
function mockEffectRegistry(): { | ||
mockEffect: string => () => () => void, | ||
mockEffectWithoutCleanup: string => () => void, | ||
registry: $ReadOnlyArray<TestEffect | TestEffectCleanup>, | ||
} { | ||
const registry = []; | ||
return { | ||
mockEffect(name: string): () => () => void { | ||
return instance => { | ||
const key = instance?.props?.testID; | ||
registry.push(new TestEffect(name, key)); | ||
return () => { | ||
registry.push(new TestEffectCleanup(name, key)); | ||
}; | ||
}; | ||
}, | ||
mockEffectWithoutCleanup(name: string): () => void { | ||
return instance => { | ||
const key = instance?.props?.testID; | ||
registry.push(new TestEffect(name, key)); | ||
}; | ||
}, | ||
registry, | ||
}; | ||
} | ||
|
||
test('calls effect without cleanup', () => { | ||
let root; | ||
|
||
const {mockEffectWithoutCleanup, registry} = mockEffectRegistry(); | ||
const effectA = mockEffectWithoutCleanup('A'); | ||
|
||
act(() => { | ||
root = create(<TestView childKey="foo" effect={effectA} />); | ||
}); | ||
|
||
expect(registry).toEqual([TestEffect.called('A', 'foo')]); | ||
|
||
act(() => { | ||
root.unmount(); | ||
}); | ||
|
||
expect(registry).toEqual([TestEffect.called('A', 'foo')]); | ||
}); | ||
|
||
test('calls effect and cleanup', () => { | ||
let root; | ||
|
||
const {mockEffect, registry} = mockEffectRegistry(); | ||
const effectA = mockEffect('A'); | ||
|
||
act(() => { | ||
root = create(<TestView childKey="foo" effect={effectA} />); | ||
}); | ||
|
||
expect(registry).toEqual([TestEffect.called('A', 'foo')]); | ||
|
||
act(() => { | ||
root.unmount(); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
]); | ||
}); | ||
|
||
test('cleans up old effect before calling new effect', () => { | ||
let root; | ||
|
||
const {mockEffect, registry} = mockEffectRegistry(); | ||
const effectA = mockEffect('A'); | ||
const effectB = mockEffect('B'); | ||
|
||
act(() => { | ||
root = create(<TestView childKey="foo" effect={effectA} />); | ||
}); | ||
|
||
act(() => { | ||
root.update(<TestView childKey="foo" effect={effectB} />); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
TestEffect.called('B', 'foo'), | ||
]); | ||
|
||
act(() => { | ||
root.unmount(); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
TestEffect.called('B', 'foo'), | ||
TestEffectCleanup.called('B', 'foo'), | ||
]); | ||
}); | ||
|
||
test('calls cleanup and effect on new instance', () => { | ||
let root; | ||
|
||
const {mockEffect, registry} = mockEffectRegistry(); | ||
const effectA = mockEffect('A'); | ||
|
||
act(() => { | ||
root = create(<TestView childKey="foo" effect={effectA} />); | ||
}); | ||
|
||
act(() => { | ||
root.update(<TestView childKey="bar" effect={effectA} />); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
TestEffect.called('A', 'bar'), | ||
]); | ||
|
||
act(() => { | ||
root.unmount(); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
TestEffect.called('A', 'bar'), | ||
TestEffectCleanup.called('A', 'bar'), | ||
]); | ||
}); | ||
|
||
test('cleans up old effect before calling new effect with new instance', () => { | ||
let root; | ||
|
||
const {mockEffect, registry} = mockEffectRegistry(); | ||
const effectA = mockEffect('A'); | ||
const effectB = mockEffect('B'); | ||
|
||
act(() => { | ||
root = create(<TestView childKey="foo" effect={effectA} />); | ||
}); | ||
|
||
act(() => { | ||
root.update(<TestView childKey="bar" effect={effectB} />); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
TestEffect.called('B', 'bar'), | ||
]); | ||
|
||
act(() => { | ||
root.unmount(); | ||
}); | ||
|
||
expect(registry).toEqual([ | ||
TestEffect.called('A', 'foo'), | ||
TestEffectCleanup.called('A', 'foo'), | ||
TestEffect.called('B', 'bar'), | ||
TestEffectCleanup.called('B', 'bar'), | ||
]); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
/** | ||
* Copyright (c) Facebook, Inc. and its affiliates. | ||
* | ||
* This source code is licensed under the MIT license found in the | ||
* LICENSE file in the root directory of this source tree. | ||
* | ||
* @flow strict | ||
* @format | ||
*/ | ||
|
||
import {useCallback, useRef} from 'react'; | ||
|
||
type CallbackRef<T> = T => mixed; | ||
|
||
/** | ||
* Constructs a callback ref that provides similar semantics as `useEffect`. The | ||
* supplied `effect` callback will be called with non-null component instances. | ||
* The `effect` callback can also optionally return a cleanup function. | ||
* | ||
* When a component is updated or unmounted, the cleanup function is called. The | ||
* `effect` callback will then be called again, if applicable. | ||
* | ||
* When a new `effect` callback is supplied, the previously returned cleanup | ||
* function will be called before the new `effect` callback is called with the | ||
* same instance. | ||
* | ||
* WARNING: The `effect` callback should be stable (e.g. using `useCallback`). | ||
*/ | ||
export default function useRefEffect<TInstance>( | ||
effect: TInstance => (() => void) | void, | ||
): CallbackRef<TInstance | null> { | ||
const cleanupRef = useRef<(() => void) | void>(undefined); | ||
return useCallback( | ||
instance => { | ||
if (cleanupRef.current) { | ||
cleanupRef.current(); | ||
cleanupRef.current = undefined; | ||
} | ||
if (instance != null) { | ||
cleanupRef.current = effect(instance); | ||
} | ||
}, | ||
[effect], | ||
); | ||
} |