Skip to content

Commit

Permalink
RN: Create useRefEffect Utility
Browse files Browse the repository at this point in the history
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
yungsters authored and facebook-github-bot committed Jun 7, 2021
1 parent 0b994ac commit cb25638
Show file tree
Hide file tree
Showing 2 changed files with 287 additions and 0 deletions.
242 changes: 242 additions & 0 deletions Libraries/Utilities/__tests__/useRefEffect-test.js
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'),
]);
});
45 changes: 45 additions & 0 deletions Libraries/Utilities/useRefEffect.js
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],
);
}

0 comments on commit cb25638

Please sign in to comment.