简体   繁体   中英

React useRef hook causing “Cannot read property '0' of undefined” error

I'm trying to create a reference using the useRef hook for each items within an array of `data by doing the following:

const markerRef = useRef(data.posts.map(React.createRef))

Now, data is fetched externally through GraphQL and it takes time to arrive, therefore, during the mounting phase, data is undefined . This causes the following error:

TypeError: Cannot read property '0' of undefined

I've tried the following with no success:

const markerRef = useRef(data && data.posts.map(React.createRef))

How do I set up so that I can map through the data without causing the error?

    useEffect(() => {
        handleSubmit(navigation.getParam('searchTerm', 'default value'))
    }, [])

    const [loadItems, { called, loading, error, data }] = useLazyQuery(GET_ITEMS)
    const markerRef = useRef(data && data.posts.map(React.createRef))

    const onRegionChangeComplete = newRegion => {
        setRegion(newRegion)
    }

    const handleSubmit = () => {
        loadItems({
            variables: {
                query: search
            }
        })
    }

    const handleShowCallout = index => {
       //handle logic
    }

    if (called && loading) {
        return (
            <View style={[styles.container, styles.horizontal]}>
                <ActivityIndicator size="large" color="#0000ff" />
            </View>
        )
    }   

    if (error) return <Text>Error...</Text> 

    return (
        <View style={styles.container}>
            <MapView 
                style={{ flex: 1 }} 
                region={region}
                onRegionChangeComplete={onRegionChangeComplete}
            >
                {data && data.posts.map((marker, index) => (

                        <Marker
                            ref={markerRef.current[index]}
                            key={marker.id}
                            coordinate={{latitude: marker.latitude, longitude: marker.longitude }}
                            // title={marker.title}
                            // description={JSON.stringify(marker.price)}
                        >
                            <Callout onPress={() => handleShowCallout(index)}>
                                <Text>{marker.title}</Text>
                                <Text>{JSON.stringify(marker.price)}</Text>
                            </Callout>
                        </Marker>

                ))}
            </MapView>
        </View>
    )

I'm using the useLazyQuery because I need to trigger it at different times.

Update:

I have modified the useRef to the following on the advise of @azundo:

const dataRef = useRef(data);
const markerRef = useRef([]);
if (data && data !== dataRef.current) {
    markerRef.current = data.posts.map(React.createRef);
    dataRef.current = data
}

When I console.log markerRef.current , I get the following result: 在此处输入图像描述

which is perfectly fine. However, when I attempt to map each current and invoke showCallout() to open all the callouts for each marker by doing the following:

markerRef.current.map(ref => ref.current && ref.current.showCallout())

nothing gets executed.

console.log(markerRef.current.map(ref => ref.current && ref.current.showCallout()))

This shows null for each array.

The useRef expression is only executed once per component mount so you'll need to update the refs whenever data changes. At first I suggested useEffect but it runs too late so the refs are not created on first render. Using a second ref to check to see if data changes in order to regenerate the marker refs synchronously should work instead.

const dataRef = useRef(data);
const markerRef = useRef([]);
if (data && data !== dataRef.current) {
  markerRef.current = data.posts.map(React.createRef);
  dataRef.current = data;
}

Additional edit:

In order to fire the showCallout on all of the components on mount, the refs must populated first. This might be an appropriate time for useLayoutEffect so that it runs immediately after the markers are rendered and ref values (should?) be set.

useLayoutEffect(() => {
  if (data) {
    markerRef.current.map(ref => ref.current && ref.current.showCallout());
  }
}, [data]);

Create refs using memoisation, like:

const markerRefs = useMemo(() => data && data.posts.map(d => React.createRef()), [data]);

Then render them like:

  {data &&
    data.posts.map((d, i) => (
      <Marker key={d} data={d} ref={markerRefs[i]}>
        <div>Callout</div>
      </Marker>
    ))}

And use the refs for calling imperative functions like:

  const showAllCallouts = () => {
    markerRefs.map(r => r.current.showCallout());
  };

See the working code with mocked Marker : https://codesandbox.io/s/muddy-bush-gfd82

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM