software-mansion / react-native-reanimated

React Native's Animated library reimplemented
https://docs.swmansion.com/react-native-reanimated/
MIT License
8.85k stars 1.29k forks source link

Create animated refs dynamically #1227

Open likern opened 4 years ago

likern commented 4 years ago

Description

For using infinite lists (which not only add new rows on demand, but also delete old ones) it's required to create animated refs dynamically. Now it's only allowed to use useAnimatedRef which can't be created on-demand.

Let's say I added one page (rows are grouped by pages). I wrap first and last rows of page into Animated.View to measure it's position inside onScroll.

That way I can understand the height of view , and track it's position.

If page is shown in viewport of ScrollView - I can add another page.

Caveats

Motivation

High-performance scrolling, including infinite and cycled lists.

Code Example

Animated.createAnimatedRef()
karol-bisztyga commented 4 years ago

As stated here using react hooks not in the top level of a function component is considered as a bad practice and should be avoided. If I understand correctly that excludes using them on demand like you would like to. Couldn't your example be done creating a certain amount of refs and reassigning them?

likern commented 4 years ago

As stated here using react hooks not in the top level of a function component is considered as a bad practice and should be avoided. If I understand correctly that excludes using them on demand like you would like to. Couldn't your example be done creating a certain amount of refs and reassigning them?

From my understanding it's not possible.

I use ref reassign to measure only last page. But as soon as last page is deleted, I have to somewhere get references from penultimate page (references to it's first and last rows). Not only references, but filled references (which points to actual Animated.View's). I need to store references to all first / last rows of all rendered pages.

The algorithm to decide when to add page is controlled by component customer (it's dynamic), something like this:

// Any custom logic provided by customer
const isAppendPageToEnd = (event: NativeScrollEvent) => {
  'worklet';
  const divider = 3;
  const halfOfContent = (1 - 1 / divider) * event.contentSize.height;
  const halfOfScrollView = (1 - 1 / divider) * event.layoutMeasurement.height;

  const centerYOffset = halfOfContent - event.contentOffset.y;

  if (centerYOffset < halfOfScrollView) {
    return [true, halfOfScrollView, centerYOffset];
  }
  return [false, halfOfScrollView, centerYOffset];
};

So we don't know number of pages beforehand.

My algorithm for now is the following: Whenever new page is added (with N rows, where N can be dynamic):

const TrackedRow = React.forwardRef<Animated.View, TrackedRowProps>(
  (props, ref) => {
    return <Animated.View ref={ref}>{props.children}</Animated.View>;
  }
);
let ref: React.RefObject<Animated.View> | undefined;
if (pages.size === 0) {
  if (isFirstRow) {
    ref = firstPageFirstRowRef;
  } else if (isLastRow) {
    ref = firstPageLastRowRef;
    lastPageLastRowRefData.current = data[i];
  }
} else {
  if (isFirstRow) {
    ref = lastPageFirstRowRef;
    lastPageFirstRowRefData.current = data[i];
  } else if (isLastRow) {
    ref = lastPageLastRowRef;
    // console.log(`last row: ${JSON.stringify(data[i])}`);
    lastPageLastRowRefData.current = data[i];
  }
}

const trackedRow = React.createElement(
  TrackedRow,
  {
    key,
    ref
  },
  row
);
row = trackedRow;

Draft how to delete rows by pages.

const pageIdsRef = useRef<number[]>([]);
const [pages] = useState<Map<number, Key[]>>(createMap);
const [rows] = useState<Map<Key, React.ReactElement<any>>>(createMap);

  const deleteRowsOfFirstPage = useCallback(() => {
    console.log('onFirstPageDelete: function start');

    if (pageIdsRef.current.length > 0) {
      const pageId = pageIdsRef.current.shift();
      if (pageId !== undefined) {
        const pageRows = pages.get(pageId);
        if (pageRows !== undefined) {
          pageRows.forEach((pageRow) => {
            rows.delete(pageRow);
          });
        }
      }

      rerender();
    }
  }, [rows, pages, pageIdsRef, rerender]);
likern commented 4 years ago

I think one approach would be through using https://en.reactjs.org/docs/refs-and-the-dom.html#callback-refs.

But for that to working measure should take not only references, created by useAnimatedRef, but component / tag directly (what is provided in callback reference)

jkadamczyk commented 2 years ago

Hi @likern

This issue is a bit stale, but I think you are touching on a potentially valuable topic.

Let me try to rephrase the issue a little bit to see if I understand it properly since there seems to be a lot of context, that I may be missing.

What you would like to have in reanimated 2 is another way to declare refs, apart from useAnimatedRef which is similar to useRef from React. You are vouching to add one of two:

To extend a bit, you would like to have it because you want to store multiple refs, and you don't know how many of them you want to store (perhaps how many comes from the API) and you want them to be mutable so you want to have a convenient way to re-assign them.

Let me know if I am correct here and correct me if I'm wrong.

Thank you for creating the feature request, have a nice day Victor!

likern commented 2 years ago

@jkadamczyk 👋🏻 Yes, you are right.

To extend a bit, you would like to have it because you want to store multiple refs, and you don't know how many of them you want to store (perhaps how many comes from the API) and you want them to be mutable so you want to have a convenient way to re-assign them.

Yes. Not sure about reassignment (the original code is gone).

a callback option to assign ref anywhere like with <Component ref={ref => foo = ref} /> to be used in Animated Components

Yes.

something similar to React.createRef() but for Reanimated 2

Yes.

Ability to create animated refs dynamically (for example for every list element) Ability to create at any code (in callbacks or conditionally) without hooks restrictions. It can be implemented like React.createRef(), but not required.

The original idea was

  1. React Native allows to render <View /> (for every list element or conditionally)
  2. Wrap <View /> into <Animated.View />
  3. Use that parent <Animated.View /> to track <View />
  4. Track using ref.measure() and all worklet stuff on UI thread

This Feature Request helps Implement <FlatList /> purely on UI thread using <View /> as base component. And managing adding / deleting row ourselves. Implement viewabilityConfig and onViewableItemsChanged on UI thread https://reactnative.dev/docs/virtualizedlist#onviewableitemschanged.

BryanEnid commented 2 years ago

I'm having the same issue. I need to define multiple animated "scrollviews" that are coming dynamically, but I need to have their refs in order to have access to their "scrollTo" method.

I tried something like this:

const x_refs_array = new Array(SCREENS.length).fill(useAnimatedRef());

const handleSubscreenXScroll = useAnimatedScrollHandler(() => {
    console.log(x_refs_array[0]()) //  -1
 })

// ...

return (
    <Animated.ScrollView
      // onScroll={handleScreenYScroll}
      horizontal
      ref={y_ref}
    >
      {dynamicsXScrollSections.map((content, index) => (
        <Animated.ScrollView
          // onScroll={handleSectionXScroll}
          ref={(ref) => (x_refs_array[index].current = ref)} // <----- here
        >
          {data}
        </Animated.ScrollView>
      ))}
    </Animated.ScrollView>
  );
zazagag commented 1 year ago

Hi! Is there any updates? Or maybe there are some known approaches how to achieve createRef behavior (means dynamically created animated refs)?

bohjak commented 7 months ago

I'd like to second this.

export function useAnimatedRef<T extends ComponentRef>(): RefObjectFunction<T> {
  const tag = useSharedValue<number | ShadowNodeWrapper | null>(-1);
  const ref = useRef<RefObjectFunction<T>>();

  if (!ref.current) {
    const fun: RefObjectFunction<T> = <RefObjectFunction<T>>((component) => {
      // enters when ref is set by attaching to a component
      if (component) {
        tag.value = getTagValueFunction(getComponentOrScrollableRef(component));
        fun.current = component;
      }
      return tag.value;
    });

    fun.current = null;

    const remoteRef = makeShareableCloneRecursive({
      __init: () => {
        'worklet';
        return () => tag.value;
      },
    });
    registerShareableMapping(fun, remoteRef);
    ref.current = fun;
  }

  return ref.current;
}

Looking at useAnimatedRef()'s code, I don't understand what exactly remoteRef is about. But the relevant part for scrollTo seems to be the tag number, since that's what the RefObjectFunction returns when called.

scrollTo = ( animatedRef: RefObjectFunction<Component>, x: number, y: number, animated: boolean) => {
  'worklet';
  if (!_WORKLET) { return; }

  // Calling animatedRef on Paper returns a number (nativeTag)
  const viewTag = animatedRef() as number;
  _scrollTo(viewTag, x, y, animated);
};

So I think that just exposing the ability to get a tag for a component should be enough for most usecases?

Animated.getTagForComponent<C extends ComponentRef> = (component: C) => getTagValueFunction(getComponentOrScrollableRef(component))