Skip to content

Commit

Permalink
Merge ViewDescriptors implementation (#6123)
Browse files Browse the repository at this point in the history
## Summary

The motivation behind this PR was the lack of animation in the web
implementation when React.StrictMode was enabled. While debugging, I
discovered we had two similar mechanisms for storing connection between
components and animations: one for mobile animations and one for web
animations. I realized that the native implementation worked flawlessly
without any issues on the web as well, as it had been recently
refactored to align with React's guidelines. I decided to remove the
flawed web implementation and replace it with mobile implementation.

## Test plan

1. Open web-example and check if animations works.
2. Wrap any web example from example app with
`<React.StrictMode></React.StrictMode>` and see if animations works.
  • Loading branch information
piaskowyk authored Jul 2, 2024
1 parent 4ded61f commit 13cc431
Show file tree
Hide file tree
Showing 12 changed files with 34 additions and 119 deletions.
19 changes: 8 additions & 11 deletions packages/react-native-reanimated/src/UpdateProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,24 @@ import { processColorsInProps } from './Colors';
import type { ShadowNodeWrapper, SharedValue, StyleProps } from './commonTypes';
import type { AnimatedStyle } from './helperTypes';
import type { Descriptor } from './hook/commonTypes';
import type { ReanimatedHTMLElement } from './js-reanimated';
import { _updatePropsJS } from './js-reanimated';
import { isFabric, isJest, shouldBeUseWeb } from './PlatformChecker';
import type { ViewRefSet } from './ViewDescriptorsSet';
import { runOnUIImmediately } from './threads';

let updateProps: (
viewDescriptor: SharedValue<Descriptor[]>,
updates: StyleProps | AnimatedStyle<any>,
maybeViewRef: ViewRefSet<any> | undefined,
isAnimatedProps?: boolean
) => void;

if (shouldBeUseWeb()) {
updateProps = (_, updates, maybeViewRef, isAnimatedProps) => {
updateProps = (viewDescriptors, updates, isAnimatedProps) => {
'worklet';
if (maybeViewRef) {
maybeViewRef.items.forEach((item, _index) => {
_updatePropsJS(updates, item, isAnimatedProps);
});
}
viewDescriptors.value?.forEach((viewDescriptor) => {
const component = viewDescriptor.tag as ReanimatedHTMLElement;
_updatePropsJS(updates, component, isAnimatedProps);
});
};
} else {
updateProps = (viewDescriptors, updates) => {
Expand All @@ -37,7 +35,6 @@ if (shouldBeUseWeb()) {
export const updatePropsJestWrapper = (
viewDescriptors: SharedValue<Descriptor[]>,
updates: AnimatedStyle<any>,
maybeViewRef: ViewRefSet<any> | undefined,
animatedStyle: MutableRefObject<AnimatedStyle<any>>,
adapters: ((updates: AnimatedStyle<any>) => void)[]
): void => {
Expand All @@ -49,7 +46,7 @@ export const updatePropsJestWrapper = (
...updates,
};

updateProps(viewDescriptors, updates, maybeViewRef);
updateProps(viewDescriptors, updates);
};

export default updateProps;
Expand Down Expand Up @@ -98,7 +95,7 @@ const createUpdatePropsManager = isFabric()
) {
viewDescriptors.value.forEach((viewDescriptor) => {
operations.push({
tag: viewDescriptor.tag,
tag: viewDescriptor.tag as number,
name: viewDescriptor.name || 'RCTView',
updates,
});
Expand Down
42 changes: 0 additions & 42 deletions packages/react-native-reanimated/src/ViewDescriptorsSet.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
'use strict';
import { useRef } from 'react';
import { makeMutable } from './core';
import type { SharedValue } from './commonTypes';
import type { Descriptor } from './hook/commonTypes';
import { shouldBeUseWeb } from './PlatformChecker';

export interface ViewRefSet<T> {
items: Set<T>;
add: (item: T) => void;
remove: (item: T) => void;
}

export interface ViewDescriptorsSet {
shareableViewDescriptors: SharedValue<Descriptor[]>;
Expand Down Expand Up @@ -51,37 +43,3 @@ export function makeViewDescriptorsSet(): ViewDescriptorsSet {
};
return data;
}

const SHOULD_BE_USE_WEB = shouldBeUseWeb();

export const useViewRefSet = SHOULD_BE_USE_WEB
? useViewRefSetJS
: useViewRefSetNative;

function useViewRefSetNative() {
// Stub native implementation.
return undefined;
}

function useViewRefSetJS<T>(): ViewRefSet<T> {
const ref = useRef<ViewRefSet<T> | null>(null);
if (ref.current === null) {
const data: ViewRefSet<T> = {
items: new Set(),

add: (item: T) => {
if (data.items.has(item)) {
return;
}
data.items.add(item);
},

remove: (item: T) => {
data.items.delete(item);
},
};
ref.current = data;
}

return ref.current;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,11 @@ import type {
} from './commonTypes';
import { flattenArray } from './utils';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { adaptViewConfig } from '../ConfigHelper';
import updateProps from '../UpdateProps';
import { stopMapper, startMapper } from '../mappers';
import { isSharedValue } from '../isSharedValue';
import { shouldBeUseWeb } from '../PlatformChecker';

const SHOULD_BE_USE_WEB = shouldBeUseWeb();

function isInlineStyleTransform(transform: unknown): boolean {
if (!Array.isArray(transform)) {
Expand Down Expand Up @@ -160,13 +157,10 @@ export class InlinePropManager implements IInlinePropManager {
const shareableViewDescriptors =
this._inlinePropsViewDescriptors.shareableViewDescriptors;

const maybeViewRef = SHOULD_BE_USE_WEB
? ({ items: new Set([animatedComponent]) } as ViewRefSet<unknown>) // see makeViewsRefSet
: undefined;
const updaterFunction = () => {
'worklet';
const update = getInlinePropsUpdate(newInlineProps);
updateProps(shareableViewDescriptors, update, maybeViewRef);
updateProps(shareableViewDescriptors, update);
};
this._inlineProps = newInlineProps;
if (this._inlinePropsMapperId) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ export class PropsFilter implements IPropsFilter {
const processedStyle: StyleProps = styles.map((style) => {
if (style && style.viewDescriptors) {
// this is how we recognize styles returned by useAnimatedStyle
// TODO - refactor, since `viewsRef` is only present on Web
style.viewsRef?.add(component);
if (this._requiresNewInitials) {
this._initialStyle = {
...style.initial.value,
Expand All @@ -72,8 +70,6 @@ export class PropsFilter implements IPropsFilter {
Object.keys(animatedProp.initial.value).forEach((initialValueKey) => {
props[initialValueKey] =
animatedProp.initial?.value[initialValueKey];
// TODO - refacotr, since `viewsRef` is only present on Web
animatedProp.viewsRef?.add(component);
});
}
} else if (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
StyleProps,
} from '../commonTypes';
import type { ViewConfig } from '../ConfigHelper';
import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import type {
BaseAnimationBuilder,
EntryExitAnimationFunction,
Expand All @@ -17,7 +17,6 @@ import type { SkipEnteringContext } from '../component/LayoutAnimationConfig';

export interface AnimatedProps extends Record<string, unknown> {
viewDescriptors?: ViewDescriptorsSet;
viewsRef?: ViewRefSet<unknown>;
initial?: SharedValue<StyleProps>;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,7 @@ export function createAnimatedComponent(
}

_detachStyles() {
if (IS_WEB && this._styles !== null) {
for (const style of this._styles) {
style.viewsRef.remove(this);
}
} else if (this._componentViewTag !== -1 && this._styles !== null) {
if (this._componentViewTag !== -1 && this._styles !== null) {
for (const style of this._styles) {
style.viewDescriptors.remove(this._componentViewTag);
}
Expand Down
9 changes: 3 additions & 6 deletions packages/react-native-reanimated/src/hook/commonTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import type {
ViewStyle,
NativeScrollEvent,
} from 'react-native';
import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import type { AnimatedStyle } from '../helperTypes';
import type { ReanimatedHTMLElement } from '../js-reanimated';

export type DependencyList = Array<unknown> | undefined;

export interface Descriptor {
tag: number;
tag: number | ReanimatedHTMLElement;
name: string;
shadowNodeWrapper: ShadowNodeWrapper;
}
Expand Down Expand Up @@ -96,10 +97,6 @@ export interface AnimatedStyleHandle<
value: AnimatedStyle<Style>;
updater: () => AnimatedStyle<Style>;
};
/**
* @remarks `viewsRef` is only defined in Web implementation.
*/
viewsRef: ViewRefSet<unknown> | undefined;
}

export interface JestAnimatedStyleHandle<
Expand Down
35 changes: 9 additions & 26 deletions packages/react-native-reanimated/src/hook/useAnimatedStyle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ import type {
Descriptor,
JestAnimatedStyleHandle,
} from './commonTypes';
import type { ViewDescriptorsSet, ViewRefSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet, useViewRefSet } from '../ViewDescriptorsSet';
import type { ViewDescriptorsSet } from '../ViewDescriptorsSet';
import { makeViewDescriptorsSet } from '../ViewDescriptorsSet';
import { isJest, shouldBeUseWeb } from '../PlatformChecker';
import type {
AnimationObject,
Expand Down Expand Up @@ -178,7 +178,6 @@ function styleUpdater(
viewDescriptors: SharedValue<Descriptor[]>,
updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
state: AnimatedState,
maybeViewRef: ViewRefSet<any> | undefined,
animationsActive: SharedValue<boolean>,
isAnimatedProps = false
): void {
Expand Down Expand Up @@ -234,7 +233,7 @@ function styleUpdater(
}

if (updates) {
updateProps(viewDescriptors, updates, maybeViewRef);
updateProps(viewDescriptors, updates);
}

if (!allFinished) {
Expand All @@ -252,14 +251,14 @@ function styleUpdater(
}

if (hasNonAnimatedValues) {
updateProps(viewDescriptors, nonAnimatedNewValues, maybeViewRef);
updateProps(viewDescriptors, nonAnimatedNewValues);
}
} else {
state.isAnimationCancelled = true;
state.animations = [];

if (!shallowEqual(oldValues, newValues)) {
updateProps(viewDescriptors, newValues, maybeViewRef, isAnimatedProps);
updateProps(viewDescriptors, newValues, isAnimatedProps);
}
}
state.last = newValues;
Expand All @@ -269,7 +268,6 @@ function jestStyleUpdater(
viewDescriptors: SharedValue<Descriptor[]>,
updater: WorkletFunction<[], AnimatedStyle<any>> | (() => AnimatedStyle<any>),
state: AnimatedState,
maybeViewRef: ViewRefSet<any> | undefined,
animationsActive: SharedValue<boolean>,
animatedStyle: MutableRefObject<AnimatedStyle<any>>,
adapters: AnimatedPropsAdapterFunction[]
Expand Down Expand Up @@ -326,13 +324,7 @@ function jestStyleUpdater(
});

if (Object.keys(updates).length) {
updatePropsJestWrapper(
viewDescriptors,
updates,
maybeViewRef,
animatedStyle,
adapters
);
updatePropsJestWrapper(viewDescriptors, updates, animatedStyle, adapters);
}

if (!allFinished) {
Expand All @@ -358,13 +350,7 @@ function jestStyleUpdater(
state.last = newValues;

if (!shallowEqual(oldValues, newValues)) {
updatePropsJestWrapper(
viewDescriptors,
newValues,
maybeViewRef,
animatedStyle,
adapters
);
updatePropsJestWrapper(viewDescriptors, newValues, animatedStyle, adapters);
}
}

Expand Down Expand Up @@ -423,7 +409,6 @@ export function useAnimatedStyle<Style extends DefaultStyle>(
adapters?: AnimatedPropsAdapterWorklet | AnimatedPropsAdapterWorklet[] | null,
isAnimatedProps = false
): AnimatedStyleHandle<Style> | JestAnimatedStyleHandle<Style> {
const viewsRef: ViewRefSet<unknown> | undefined = useViewRefSet();
const animatedUpdaterData = useRef<AnimatedUpdaterData>();
let inputs = Object.values(updater.__closure ?? {});
if (SHOULD_BE_USE_WEB) {
Expand Down Expand Up @@ -506,7 +491,6 @@ For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/doc
shareableViewDescriptors,
updater,
remoteState,
viewsRef,
areAnimationsActive,
jestAnimatedStyle,
adaptersArray
Expand All @@ -519,7 +503,6 @@ For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/doc
shareableViewDescriptors,
updaterFn,
remoteState,
viewsRef,
areAnimationsActive,
isAnimatedProps
);
Expand Down Expand Up @@ -547,8 +530,8 @@ For more, see the docs: \`https://docs.swmansion.com/react-native-reanimated/doc

if (!animatedStyleHandle.current) {
animatedStyleHandle.current = isJest()
? { viewDescriptors, initial, viewsRef, jestAnimatedStyle }
: { initial, viewsRef, viewDescriptors };
? { viewDescriptors, initial, jestAnimatedStyle }
: { viewDescriptors, initial };
}

return animatedStyleHandle.current;
Expand Down
14 changes: 6 additions & 8 deletions packages/react-native-reanimated/src/js-reanimated/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,17 +53,15 @@ export interface ReanimatedHTMLElement extends HTMLElement {
export const _updatePropsJS = (
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
updates: StyleProps | AnimatedStyle<any>,
viewRef: {
_component?: (JSReanimatedComponent | ReanimatedHTMLElement) & {
getAnimatableRef?: () => JSReanimatedComponent | ReanimatedHTMLElement;
};
viewRef: (JSReanimatedComponent | ReanimatedHTMLElement) & {
getAnimatableRef?: () => JSReanimatedComponent | ReanimatedHTMLElement;
},
isAnimatedProps?: boolean
): void => {
if (viewRef._component) {
const component = viewRef._component.getAnimatableRef
? viewRef._component.getAnimatableRef()
: viewRef._component;
if (viewRef) {
const component = viewRef.getAnimatableRef
? viewRef.getAnimatableRef()
: viewRef;
const [rawStyles] = Object.keys(updates).reduce(
(acc: [StyleProps, AnimatedStyle<any>], key) => {
const value = updates[key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,12 @@ export const snapshots = new WeakMap<HTMLElement, ReanimatedSnapshot>();

export function makeElementVisible(element: HTMLElement, delay: number) {
if (delay === 0) {
_updatePropsJS(
{ visibility: 'initial' },
{ _component: element as ReanimatedHTMLElement }
);
_updatePropsJS({ visibility: 'initial' }, element as ReanimatedHTMLElement);
} else {
setTimeout(() => {
_updatePropsJS(
{ visibility: 'initial' },
{ _component: element as ReanimatedHTMLElement }
element as ReanimatedHTMLElement
);
}, delay * 1000);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ export function setElementAnimation(
if (animationConfig.animationType === LayoutAnimationType.ENTERING) {
_updatePropsJS(
{ visibility: 'initial' },
{ _component: element as ReanimatedHTMLElement }
element as ReanimatedHTMLElement
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export function setNativeProps<T extends Component>(
updates: StyleProps
) {
const component = animatedRef() as ReanimatedHTMLElement;
_updatePropsJS(updates, { _component: component });
_updatePropsJS(updates, component);
}

0 comments on commit 13cc431

Please sign in to comment.