'use strict'; import type { ComponentProps, Ref } from 'react'; import { Component } from 'react'; import type { StyleProp } from 'react-native'; import { Platform, StyleSheet } from 'react-native'; import type { AnyComponent, AnyRecord, PlainStyle } from '../../common'; import { IS_JEST, ReanimatedError, SHOULD_BE_USE_WEB } from '../../common'; import type { InternalHostInstance, ShadowNodeWrapper, } from '../../commonTypes'; import type { AnimatedComponentRef, IAnimatedComponentInternalBase, ViewInfo, } from '../../createAnimatedComponent/commonTypes'; import { getViewInfo } from '../../createAnimatedComponent/getViewInfo'; import { getShadowNodeWrapperFromRef } from '../../fabricUtils'; import { findHostInstance } from '../../platform-specific/findHostInstance'; import { markNodeAsRemovable, unmarkNodeAsRemovable } from '../native'; import { CSSManager } from '../platform'; import type { CSSStyle } from '../types'; import { filterNonCSSStyleProps } from './utils'; export type AnimatedComponentProps = Record & { ref?: Ref; style?: StyleProp; }; // TODO - change these ugly underscore prefixed methods and properties to real // private/protected ones when possible (when changes from this repo are merged // to the main one) export default class AnimatedComponent< P extends AnyRecord = AnimatedComponentProps, S extends AnyRecord = Record, > extends Component implements IAnimatedComponentInternalBase { ChildComponent: AnyComponent; _CSSManager?: CSSManager; _viewInfo?: ViewInfo; _cssStyle: CSSStyle = {}; // RN style object with Reanimated CSS properties _componentRef: AnimatedComponentRef | HTMLElement | null = null; _hasAnimatedRef = false; // Used only on web _componentDOMRef: HTMLElement | null = null; _willUnmount: boolean = false; constructor(ChildComponent: AnyComponent, props: P) { super(props); this.ChildComponent = ChildComponent; } getComponentViewTag() { return this._getViewInfo().viewTag as number; } _onSetLocalRef() { // noop - can be overridden in subclasses } _getViewInfo(): ViewInfo { if (this._viewInfo !== undefined) { return this._viewInfo; } let viewTag: number | typeof this._componentRef; let shadowNodeWrapper: ShadowNodeWrapper | null = null; let DOMElement: HTMLElement | null = null; let viewName: string | undefined; if (SHOULD_BE_USE_WEB) { // At this point we assume that `_setComponentRef` was already called and `_component` is set. // `this._component` on web represents HTMLElement of our component, that's why we use casting // TODO - implement a valid solution later on - this is a temporary fix viewTag = this._componentRef; DOMElement = this._componentDOMRef; } else { const hostInstance = findHostInstance(this); if (!hostInstance) { /* findHostInstance can return null for a component that doesn't render anything (render function returns null). Example: svg Stop: https://github.com/react-native-svg/react-native-svg/blob/develop/src/elements/Stop.tsx */ throw new ReanimatedError( 'Cannot find host instance for this component. Maybe it renders nothing?' ); } const viewInfo = getViewInfo(hostInstance); viewTag = viewInfo.viewTag ?? -1; viewName = viewInfo.viewName; shadowNodeWrapper = getShadowNodeWrapperFromRef( this as InternalHostInstance, hostInstance ); } this._viewInfo = { viewTag, shadowNodeWrapper, viewName }; if (DOMElement) { this._viewInfo.DOMElement = DOMElement; } return this._viewInfo; } _setComponentRef = (ref: Component | HTMLElement) => { const forwardedRef = this.props.forwardedRef; // Forward to user ref prop (if one has been specified) if (typeof forwardedRef === 'function') { // Handle function-based refs. String-based refs are handled as functions. forwardedRef(ref); } else if (typeof forwardedRef === 'object' && forwardedRef) { // Handle createRef-based refs forwardedRef.current = ref; } if (!ref) { // component has been unmounted return; } if (ref !== this._componentRef) { this._componentRef = this._resolveComponentRef(ref); // if ref is changed, reset viewInfo this._viewInfo = undefined; } this._onSetLocalRef(); }; _resolveComponentRef = (ref: Component | HTMLElement | null) => { const componentRef = ref as AnimatedComponentRef; // Component can specify ref which should be animated when animated version of the component is created. // Otherwise, we animate the component itself. if (componentRef && componentRef.getAnimatableRef) { this._hasAnimatedRef = true; return componentRef.getAnimatableRef(); } // Case for SVG components on Web if (SHOULD_BE_USE_WEB) { if (componentRef && componentRef.elementRef) { this._componentDOMRef = componentRef.elementRef.current; } else { this._componentDOMRef = ref as HTMLElement; } } return componentRef; }; _updateStyles(props: P) { this._cssStyle = StyleSheet.flatten(props.style) ?? {}; } componentDidMount() { this._updateStyles(this.props); const viewTag = this._viewInfo?.viewTag; if ( !SHOULD_BE_USE_WEB && this._willUnmount && typeof viewTag === 'number' ) { unmarkNodeAsRemovable(viewTag); } if (!IS_JEST) { this._CSSManager ??= new CSSManager(this._getViewInfo()); this._CSSManager?.update(this._cssStyle); } this._willUnmount = false; } componentWillUnmount() { if (!IS_JEST && this._CSSManager) { this._CSSManager.unmountCleanup(); } const wrapper = this._viewInfo?.shadowNodeWrapper; if (!SHOULD_BE_USE_WEB && wrapper) { // Mark node as removable on the native (C++) side, but only actually remove it // when it no longer exists in the Shadow Tree. This ensures proper cleanup of // animations/transitions/props while handling cases where the node might be // remounted (e.g., when frozen) after componentWillUnmount is called. markNodeAsRemovable(wrapper); } this._willUnmount = true; } shouldComponentUpdate(nextProps: P) { this._updateStyles(nextProps); if (this._CSSManager) { this._CSSManager.update(this._cssStyle); } // TODO - maybe check if the render is necessary instead of always returning true return true; } render(props?: ComponentProps) { const { ChildComponent } = this; const platformProps = Platform.select({ web: {}, default: { collapsable: false }, }); return ( void} /> ); } }