netceteragroup / skele

Architectural framework that assists building data-driven apps with React or React Native.
MIT License
163 stars 32 forks source link

Cannot get ref (useRef) for <FlatList /> wrapped inside a <Viewport.Tracker> using functional component #153

Open bantingGamer opened 4 years ago

bantingGamer commented 4 years ago

Cannot get ref for <FlatList /> wrapped inside a </Viewport.Tracker> using functional component

         const flatListRef = useRef();

        <Viewport.Tracker>
          <FlatList
            ref={flatListRef}
            data={chatMessages}
            renderItem={({ item: message }) => renderItem(message)}
            keyExtractor={(item) => `${item.id}-message`}
          />
        </Viewport.Tracker>

but when using a class component it works fine

        <Viewport.Tracker>
          <FlatList
            ref={(ref) => (this.flatListRef = ref)}
          />
        </Viewport.Tracker>

I have tried React.forwardRef but that doesnt seem to work either. As soon as i remove it works

Please can you assist?

kind Regards Luke

ognen commented 4 years ago

Hi @bantingGamer,

To be honest, these components were never tested with hooks. They (still) work using a HoC approach and further more will try to hijack the ref of the inner component for their own use (i.e the HoC are not "clean").

We'v been thinking about implementing a hooks-based version of these but never had the time. If you like you could attempt to help there with a PR. @bevkoski or @kern3lpan1c might be also interested in helping out

bantingGamer commented 4 years ago

Thank you for getting back to me, I really appreciate it. 

I thought it wouldn't be the worst thing to refactor and use a Class Component.

Concerning your request for me to attempt to convert it to use hooks, is a little beyond my scope, Im a bit of a newbie when it comes to coding, I wouldn't even know where to begin :P.

Have a wonderful day kind regards Luke

ognen commented 4 years ago

Thank you too @bantingGamer, I will keep this open still.

brax10ward commented 4 years ago

@bantingGamer When you got it working by using a class component were you creating the ref variable with React.createRef()?

I tried your solution but am still getting errors. 😞

bantingGamer commented 4 years ago

Hey @brax10ward

to answer your question, i am not using createRef(). can you please provide sample code that isnt working, ideally a https://snack.expo.io/ would be awesome?

this should do the trick in a class component.

            <View>
             <Viewport.Tracker>
              <FlatList
                ref={(ref) => (this.flatList = ref)}
                data={DATA}
                renderItem={this.renderItem}
                keyExtractor={(item) => item.id}
              />
             </Viewport.Tracker>
              <Button
                title={'FlatList ref button'}
                onPress={() => {
                  console.log('flatList ref value: ', this.flatList);
                }}
              />
            </View>
brax10ward commented 4 years ago

Thanks for the response @bantingGamer. I will see if I can provide a sample on snack. Can you tell me how your are creating this.flatlist?

bantingGamer commented 4 years ago

@brax10ward I believe that this is the line where it is created. ref={(ref) => (this.flatList = ref)}

The this keyword is very confusing, I dont fully understand it myself that is why I prefer using functional components. You might want to brush up on how it works

brax10ward commented 4 years ago

@bantingGamer Can you share your whole component?

bantingGamer commented 4 years ago

@brax10ward sure here is the entire component snack example
you can just add

   <Viewport.Tracker>
...
   </Viewport.Tracker>

and it should work exactly the same

import React, { Component } from 'react';
import { View, FlatList, StyleSheet, Text, Button } from 'react-native';

const DATA = [
  {
    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abbs28ba',
    title: 'item one',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aass97f63',
    title: 'item two',
  },
  {
    id: '58694a0f-3da1-471f-bd96-145571esss29d72',
    title: 'item three',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-123546',
    title: 'item four',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-45678888',
    title: 'item five',
  },
  {
    id: '58694a0f-3da1-471f-bd96-1455767867867861e29d72',
    title: 'item six',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-fdds',
    title: 'item six',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-fbd91aa9kkukfuk7f63',
    title: 'item seven',
  },
  {
    id: '58694a0f-3da1-471f-bd96-23dfghh',
    title: 'item eight',
  },
  {
    id: 'bd7acbea-c1b1-46c2-aed5-6776899',
    title: 'item nine',
  },
  {
    id: '3ac68afc-c605-48d3-a4f8-111222',
    title: 'item ten',
  },
  {
    id: '58694a0f-3da1-471f-bd96-1234',
    title: 'item eleven',
  },
];

const Item = ({ title }) => (
  <View style={styles.item}>
    <Text style={styles.title}>{title}</Text>
  </View>
);

const renderItem = ({ item }) => <Item title={item.title} />;

class App extends Component {
  render() {
    return (
      <View
        collapsable={false}
        style={{
          flex: 1,
          alignItems: 'center',
          width: '100%',
          // backgroundColor: 'red'
        }}>
        <FlatList
          style={{
            height: 100,
          }}
          ref={(ref) => (this.flatList = ref)}
          data={DATA}
          renderItem={renderItem}
          keyExtractor={(item) => item.id}
        />
        <Button
          title={'press to scroll'}
          onPress={() => {
            this.flatList.scrollToIndex({ index: 0 }); // scroll a bit before pressing the button
            // or
            // this.flatList.scrollToOffset({ offset: 300 });
          }}
        />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  item: {
    backgroundColor: '#f9c2ff',
    padding: 20,
    marginVertical: 8,
    marginHorizontal: 16,
  },
  title: {
    fontSize: 32,
  },
});

export default App;
servonlewis commented 4 years ago

Any update on this? I am having the issue with react navigation 5, useScrollToTop(). It takes a ref of the flatlist, which is not working in a function component

servonlewis commented 4 years ago

@brax10ward sure here is the entire component snack example

you can just add


   <Viewport.Tracker>

...

   </Viewport.Tracker>

and it should work exactly the same


import React, { Component } from 'react';

import { View, FlatList, StyleSheet, Text, Button } from 'react-native';

const DATA = [

  {

    id: 'bd7acbea-c1b1-46c2-aed5-3ad53abbs28ba',

    title: 'item one',

  },

  {

    id: '3ac68afc-c605-48d3-a4f8-fbd91aass97f63',

    title: 'item two',

  },

  {

    id: '58694a0f-3da1-471f-bd96-145571esss29d72',

    title: 'item three',

  },

  {

    id: 'bd7acbea-c1b1-46c2-aed5-123546',

    title: 'item four',

  },

  {

    id: '3ac68afc-c605-48d3-a4f8-45678888',

    title: 'item five',

  },

  {

    id: '58694a0f-3da1-471f-bd96-1455767867867861e29d72',

    title: 'item six',

  },

  {

    id: 'bd7acbea-c1b1-46c2-aed5-fdds',

    title: 'item six',

  },

  {

    id: '3ac68afc-c605-48d3-a4f8-fbd91aa9kkukfuk7f63',

    title: 'item seven',

  },

  {

    id: '58694a0f-3da1-471f-bd96-23dfghh',

    title: 'item eight',

  },

  {

    id: 'bd7acbea-c1b1-46c2-aed5-6776899',

    title: 'item nine',

  },

  {

    id: '3ac68afc-c605-48d3-a4f8-111222',

    title: 'item ten',

  },

  {

    id: '58694a0f-3da1-471f-bd96-1234',

    title: 'item eleven',

  },

];

const Item = ({ title }) => (

  <View style={styles.item}>

    <Text style={styles.title}>{title}</Text>

  </View>

);

const renderItem = ({ item }) => <Item title={item.title} />;

class App extends Component {

  render() {

    return (

      <View

        collapsable={false}

        style={{

          flex: 1,

          alignItems: 'center',

          width: '100%',

          // backgroundColor: 'red'

        }}>

        <FlatList

          style={{

            height: 100,

          }}

          ref={(ref) => (this.flatList = ref)}

          data={DATA}

          renderItem={renderItem}

          keyExtractor={(item) => item.id}

        />

        <Button

          title={'press to scroll'}

          onPress={() => {

            this.flatList.scrollToIndex({ index: 0 }); // scroll a bit before pressing the button

            // or

            // this.flatList.scrollToOffset({ offset: 300 });

          }}

        />

      </View>

    );

  }

}

const styles = StyleSheet.create({

  item: {

    backgroundColor: '#f9c2ff',

    padding: 20,

    marginVertical: 8,

    marginHorizontal: 16,

  },

  title: {

    fontSize: 32,

  },

});

export default App;

I see that you are using it fine as a class component, but how can we make it work with a function component. When assigning a ref to the flatlist, it gets ignored inside the viewport wrapper.

bantingGamer commented 4 years ago

Hi @bantingGamer,

To be honest, these components were never tested with hooks. They (still) work using a HoC approach and further more will try to hijack the ref of the inner component for their own use (i.e the HoC are not "clean").

We'v been thinking about implementing a hooks-based version of these but never had the time. If you like you could attempt to help there with a PR. @bevkoski or @kern3lpan1c might be also interested in helping out

@servonlewis this is currently an issue.

servonlewis commented 4 years ago

Hey guys,

for all you react hook users, I was able to use the native controls from within Flatlist to achieve a viewport checker.

if interested, below is the code i used to make it work.

I am now able to use the flatlist ref, and keep my react hooks

// flatlist, requires viewabilityConfig and onViewableItemsChanged
<FlatList
      scrollEventThrottle={16}
      {...{
        onViewableItemsChanged,
        keyExtractor,
        data,
        onScroll,
        ref,
        renderItem,
        refreshing,
        onRefresh,
        viewabilityConfig,
      }}
    />

const viewabilityConfig = {
  waitForInteraction: true,
  itemVisiblePercentThreshold: 30
};

//use some state to capture the id of the viewport you are capturing
 const [videoPlaying, setVideoPlaying] = useState<number | null>(null);

//useCallback is import because this function cannot rerender or it'll error, as per docs.
//for me, i only wanted to capture the viewport of an item that has the video prop.
  const onViewableItemsChanged = useCallback(
    (obj: { viewableItems: ViewToken[]; changed: ViewToken[] }) => {
      const isPlaying = obj.viewableItems?.find((x) => {
        const item: Props = x?.item;
        return !!item?.video;
      });
      isPlaying
        ? setVideoPlaying((isPlaying?.item as Props)?.id)
        : setVideoPlaying(null);
    },
    []
  );

//pass that state to your component, then elsewhere, I created my video player component like this, where it only plays if the video prop, and the id matches.

interface Props {
  id: number;
  videoPlaying?: number | null;
}

export const VideoForCard = ({ id, videoPlaying }: Props) => {
  const [shouldPlay, setShouldPlay] = useState(false);
  const isFocused = useIsFocused();

  useEffect(() => {
    id === videoPlaying ? setShouldPlay(true) : setShouldPlay(false);
  }, [id, videoPlaying]);

  useEffect(() => {
    if (!isFocused) setShouldPlay(false);
    if (isFocused && id === videoPlaying) setShouldPlay(true);
  }, [isFocused]);

  return (
    <StyledVideo
      source={{
        uri: "http://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4",
      }}
      rate={1.0}
      volume={1.0}
      resizeMode="cover"
      isMuted={true}
      useNativeControls
      {...{ preTriggerRatio, shouldPlay }}
    />
  );
};
kennym commented 3 years ago

I'm having the same issue with functional components.

I was able to get this working with hooks, by using the following syntax:

<ScrollView ref={ref => (scrollView.current = ref)}

instead of

<ScrollView ref={scrollView}>

🙌

Brenndoerfer commented 2 years ago

This worked for me

const flatList = useRef();

<FlatList ref={flatList}  />

<Button
        title="next"
        onPress={() => flatList.current.scrollToIndex({ index: 1 })}
/>