Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,39 @@ describe('Animated', () => {
value1.setValue(7);
expect(listener.mock.calls.length).toBe(4);
});

it('should keep listeners when the last attached node detaches', () => {
const value1 = new Animated.Value(0);
const listener = jest.fn();
value1.addListener(listener);

const node = new AnimatedProps({style: {opacity: value1}}, () => {});
node.__attach();
node.__detach();

expect(value1.__getChildren().length).toBe(0);
expect(value1.hasListeners()).toBe(true);

value1.setValue(42);
expect(listener).toBeCalledWith({value: 42});
expect(listener.mock.calls.length).toBe(1);
});

it('should keep listeners when a bound component unmounts', async () => {
const value1 = new Animated.Value(0);
const listener = jest.fn();
value1.addListener(listener);

const root = await create(
<Animated.View style={{transform: [{translateX: value1}]}} />,
);
await unmount(root);
jest.runAllTicks();

value1.setValue(42);
expect(listener).toBeCalledWith({value: 42});
expect(listener.mock.calls.length).toBe(1);
});
});

describe('Animated Diff Clamp', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ export default class AnimatedNode {

__attach(): void {}
__detach(): void {
this.removeAllListeners();
// NOTE: Listeners registered with `addListener` are owned by the caller,
// not by this node. An `Animated.Value` can outlive every component that
// uses it, so detaching from the graph must not remove them. Subclasses
// are responsible for tearing down any listening state they own (e.g.
// `AnimatedValue` stops listening to native value updates) before the
// native node is dropped below.
if (this.__isNative && this.__nativeTag != null) {
NativeAnimatedHelper.API.dropAnimatedNode(this.__nativeTag);
this.__nativeTag = undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,10 @@ export default class AnimatedValue extends AnimatedWithChildren {
});
}
this.stopAnimation();
// Stop listening to native value updates before the native node is
// dropped by `super.__detach()`. This releases the subscription this node
// owns without discarding listeners registered by the caller.
this._updateSubscription?.remove();
super.__detach();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,65 @@ describe('Native Animated', () => {
});
expect(listener).toHaveBeenCalledTimes(4);
});

it('should stop listening to native updates on unmount, but keep listeners', async () => {
const {Animated, NativeAnimatedHelper} = importModules();

const value1 = new Animated.Value(0, {useNativeDriver: true});
const listener = jest.fn();
value1.addListener(listener);

const tag = value1.__getNativeTag();
expect(
NativeAnimatedModule.startListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(tag);

const root = await create(<Animated.View style={{opacity: value1}} />);
await unmount(root);
jest.runAllTicks();

// The subscription this node owns is released before the native node is
// dropped.
expect(
NativeAnimatedModule.stopListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(tag);
expect(NativeAnimatedModule.dropAnimatedNode).toHaveBeenCalledWith(tag);
NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 42,
tag,
});
expect(listener).not.toHaveBeenCalled();

// The caller's listener is not discarded.
expect(value1.hasListeners()).toBe(true);
});

it('should resume delivering native updates when remounted', async () => {
const {Animated, NativeAnimatedHelper} = importModules();

const value1 = new Animated.Value(0, {useNativeDriver: true});
const listener = jest.fn();
value1.addListener(listener);

const root = await create(<Animated.View style={{opacity: value1}} />);
await unmount(root);
jest.runAllTicks();

await create(<Animated.View style={{opacity: value1}} />);
jest.runAllTicks();

const tag = value1.__getNativeTag();
expect(
NativeAnimatedModule.startListeningToAnimatedNodeValue,
).toHaveBeenCalledWith(tag);

NativeAnimatedHelper.nativeEventEmitter.emit('onAnimatedValueUpdate', {
value: 42,
tag,
});
expect(listener).toBeCalledWith({value: 42});
expect(listener).toHaveBeenCalledTimes(1);
});
});

describe('Animated Events', () => {
Expand Down