6

I'm new to React so I hope I'm approaching this problem correctly. First I have a screen called SearchLocationsScreen. Inside that screen I have a component called Map and inside of Map I have custom marker components called LocationMarker. At the same hierarchical level as the Map component, I have a custom ModalBox called CheckinModal. Here is a crude diagram to help:

enter image description here

In SearchLocationsScreen I am getting the location info from an API call. Then I pass those locations down to my Map component. Inside my Map component I pass the information for the markers down to the custom LocationMarker class and populate the map.

The goal is to press on a marker and have the CheckinModal pop up from the bottom and populate it with info from the particular marker that was pressed. To do this, I use the useRef hook and forwardRef hook to pass a reference to the modal down to the LocationMarker class. Here I call ref.current.open() and the modal opens as expected.

The problem is that I can't figure out a way to pass the location info from the marker, back up the hierarchy to the screen and down to the modal to populate the modal with relevant info. Does anyone know how to achieve this? I'm posting the code below to my screen, map component and marker component (styles not included). Thanks in advance for your help.

SearchLocationsScreen.js

const SearchLocationsScreen = ({isFocused, navigation}) => {

    const {updateLocation} = useContext(CurrentLocationContext);

    // hooks
    const callback = useCallback((location) => {
        updateLocation(location)
    }, []);
    const [err] = useCurrentLocation(isFocused, callback);
    const [businessLocations] = useGetLocations();

    const modalRef = useRef(null);

    let locations = [];

    if (businessLocations) {
        for (let x = 0; x < businessLocations.length; x++) {
            locations.push({
                ...businessLocations[x],
                latitude: businessLocations[x].lat,
                longitude: businessLocations[x].lng,
                key: x,
            })
        }
    }

    return (
        <View style={{flex: 1}}>

            <Map markers={locations} ref={modalRef}/>

            <SearchBar style={styles.searchBarStyle}/>

            {err ? <View style={styles.errorContainer}><Text
                style={styles.errorMessage}>{err.message}</Text></View> : null}

            <CheckinModal
                ref={modalRef}
            />

        </View>
    );
};

Map.js

    const Map = ({markers}, ref) => {

        const {state: {currentLocation}} = useContext(Context);

        // todo figure out these error situations
        if (!currentLocation) {
            return (
                <View style={{flex: 1}}>
                    <MapView
                        style={styles.map}
                        provider={PROVIDER_GOOGLE}
                        initialRegion={{
                            latitude: 27.848680,
                            longitude: -82.646560,
                            latitudeDelta: regions.latDelta,
                            longitudeDelta: regions.longDelta
                        }}
                    />

                    <ActivityIndicator size='large' style={styles.indicator} />
                </View>
            )
        }

        return (

            <MapView
                style={styles.map}
                provider={PROVIDER_GOOGLE}
                initialRegion={{
                    ...currentLocation.coords,
                    latitudeDelta: regions.latDelta,
                    longitudeDelta: regions.longDelta
                }}
                showsUserLocation
                >

                { markers ? markers.map((marker, index) => {
                    return <LocationMarker
                        ref={ref}  // passing the ref down to the markers
                        key={index}
                        coordinate={marker}
                        title={marker.company}
                        waitTime={ marker.wait ? `${marker.wait} minutes` : 'Open'}
                    />;
                }) : null}

            </MapView>
        )
    };

    const forwardMap = React.forwardRef(Map);

    export default forwardMap;

LocationMarker.js

const LocationMarker = ({company, coordinate, title, waitTime, onShowModal}, ref) => {
    return (
        <View>
            <Marker
                coordinate={coordinate}
                title={title}
                onPress={() => {
                    console.log(ref);
                    ref.current.open();
                }}
            >
                <Image
                    source={require('../../assets/marker2.png')}
                    style={styles.locationMarker}/>
                <View style={styles.waitContainer}><Text style={styles.waitText}>{waitTime}</Text></View>
            </Marker>

        </View>
    )
};

const forwardMarker = React.forwardRef(LocationMarker);

export default forwardMarker;
Jo Momma
  • 1,126
  • 15
  • 31

3 Answers3

2

If I understood correctly, instead of using forwardRef to pass the ref from the parent using the ref prop, I suggest passing it as a simple prop. When it reaches the nested component (LocationMarker in your case) you can assign it. This is a simplified version:

const SearchLocationsScreen = props => {
    const marker_ref = useRef(null);
    const modal_ref = useRef(null);

    return (
        <View>
            <Map marker_ref={marker_ref} modal_ref={modal_ref} />
            <CheckinModal marker_ref={marker_ref} modal_ref={modal_ref} />
        </View>
    );
};

const Map = props => {
    const { marker_ref, modal_ref } = props;

    return <LocationMarker marker_ref={marker_ref} modal_ref={modal_ref} />;
};

const LocationMarker = props => {
    const { marker_ref, modal_ref } = props;

    return <div ref={marker_ref}  />;
};

const CheckinModal = props => {
    const { marker_ref, modal_ref } = props;

    return <div ref={modal_ref}  />;
};

When the ref reaches the final element we assign it using ref=. Remember that this final element needs to be a JSX element, like a div, and not a component.

To avoid passing these props from the grandparent to the children through each component in between, you could use a Context in SearchLocationsScreen.

Alvaro
  • 9,247
  • 8
  • 49
  • 76
  • I don't think this solves my problem. First, I'm using ReactNative...no
    elements but I'm sure I could modify that. Second, my markers are created inside the Map component not in the main screen. Keep in mind, the ModalBox and the Map are at the same level (no parent child relationship there) so I'm trying to propagate the info up the hierarchy and then down one level to a different tree. Did that clear it up or am I still wording it a bit confusingly.
    – Jo Momma Oct 07 '19 at 18:08
  • 1
    @JoMomma Im not sure about ReactNative. The idea is to create the values in the closest common ancestor, pass it down and assign it where corresponding. This way you dont need to propagate up, as its already created up, just modified in a deeper level. Am I understanding it? – Alvaro Oct 07 '19 at 18:18
  • Yeah, maybe I need to implement this a bit differently.....Let me try some stuff. Maybe I can create the Markers in the screen and pass them down to the Map component. Then they will be on the same level as the Modal. Perhaps that would be easier – Jo Momma Oct 07 '19 at 18:38
0

Have you thought about using a hook? Hooks allow you to compose functionally into your components without having to pass everything down via props. If you need to use this value in multiple places it might make your life easier? In your situation, you set the hook in one place and then update your other component when it changes, without worrying about passing anything around

https://reactjs.org/docs/hooks-intro.html

Jon Jones
  • 1,014
  • 1
  • 9
  • 17
0

I figured it out with the help of Alvaro's comment to my main post.

Here is what I did. First, I moved the code that generates the LocationMarkers to the SearchLocationsScreen. I was already accessing the locations needed for those markers on that screen anyway (originally I was then passing those locations down to the Map component and creating them there). In the SearchLocationsScreen I loop through all the locations to generate the LocationMarkers, adding a callback which uses a useReducer hook to store the modal's state. Since they are both on the same level, I can populate the fields for the modal with the correct data from the reducer's state. That callback is passed down to the LocationMarkers. Then in the LocationMarker onPress I call this method. Everything works as expected. Here is the updated code:

SearchLocationsScreen

const SearchLocationsScreen = ({isFocused, navigation}) => {

    const {updateLocation} = useContext(CurrentLocationContext);

    // hooks
    const callback = useCallback((location) => {
        updateLocation(location)
    }, []);
    const [err] = useCurrentLocation(isFocused, callback);
    const [businessLocations] = useGetLocations();
    const modalRef = useRef(null);

    let locations = [];

    if (businessLocations) {
        for (let x = 0; x < businessLocations.length; x++) {
            locations.push({
                ...businessLocations[x],
                latitude: businessLocations[x].lat,
                longitude: businessLocations[x].lng,
                key: x,
            })
        }
    }

    const modalReducer = (state, action) => {
        console.log("payload: ", action.payload);
        switch (action.type) {
            case 'show_modal':
                return {...state,
                    companyName: action.payload.companyName,
                    companyAddress: action.payload.companyAddress,
                    waitTime: action.payload.waitTime
                };
            default:
                return state;
        }
    };

    const [modalState, dispatch] = useReducer(modalReducer, {
        companyName: "Company Name",
        companyAddress: "123 Elm St",
        waitTime: 0
    });

    const createMarkers = () => {
        let result = [];
        if (locations) {
            for (let i = 0; i < locations.length; i++) {
                result.push(
                    <LocationMarker
                        key={i}
                        id={i}
                        coordinate={locations[i]}
                        title={locations[i].company}
                        waitTime={locations[i].wait ? `${locations[i].wait} minutes` : 'Closed'}
                        onShowModal={() => {
                            dispatch({
                                type: 'show_modal', payload: {
                                    companyName: locations[i].company,
                                    companyAddress: locations[i].address,
                                    waitTime: locations[i].wait,
                                }
                            });
                            modalRef.current.open();
                        }}
                    />
                )
            }
        }
        return result;
    };

    return (
        <View style={{flex: 1}}>

            <Map markers={createMarkers()}/>
            {/*<Map ref={modalRef} markers={...createMarkers()} />*/}

            <SearchBar style={styles.searchBarStyle}/>

            {err ? <View style={styles.errorContainer}><Text
                style={styles.errorMessage}>{err.message}</Text></View> : null}

            <CheckinModal
                ref={modalRef}
                businessName={modalState.companyName}
                address={modalState.companyAddress}
                waitTime={modalState.waitTime}
            />

        </View>
    );
};

Map

const Map = ({markers}, ref) => {

    const {state: {currentLocation}} = useContext(Context);

    return (

        <MapView
            style={styles.map}
            provider={PROVIDER_GOOGLE}
            initialRegion={{
                ...currentLocation.coords,
                latitudeDelta: regions.latDelta,
                longitudeDelta: regions.longDelta
            }}
            showsUserLocation
            >

            {markers ? markers.map((marker, index) => {
                return marker;
            }): null}

        </MapView>
    )
};

export default Map;

CheckinModal

const CheckinModal = ({businessName, address, waitTime}, ref) => {

    return (
            <ModalBox
                style={styles.modal}
                position={'bottom'}
                backdrop={true}
                ref={ref}
            >
                <Text>Store Name: {businessName}</Text>
                <Text>Store Address: {address}</Text>
                <Text>Wait time: {waitTime} minutes</Text>
            </ModalBox>
    )
};

const forwardModal = React.forwardRef(CheckinModal);
export default forwardModal;

LocationMarker

const LocationMarker = (props) => {

    return (
        <View>
            <Marker
                coordinate={props.coordinate}
                title={props.title}
                id={props.id}
                onPress={() => {
                    props.onShowModal();
                }}
            >
                <Image
                    source={require('../../assets/marker2.png')}
                    style={styles.locationMarker}/>
                <View style={styles.waitContainer}>
                    <Text style={styles.waitText}>{props.waitTime}</Text>
                </View>
            </Marker>

        </View>
    )
};

export default LocationMarker;

With this new code, I don't have to forward refs as much, only to the CheckinModal.

If anyone has any questions post them below this answer and I will try to answer them as soon as I can. Thank you everyone for your help.

Jo Momma
  • 1,126
  • 15
  • 31