0

I'm trying to control the state of 2 components use useContext() in React-Native. I have a flatmap of cards that render on a screen. Each card has a press-able 'interested' icon. If I click on a card, it shows the details of the card and also the press-able 'interested' icon. Both should share the same state of being pressed and also send a 'interested bool to the backend.

Right now, if I press 'interested' on one card, the all get pressed. Same with the inside icon of the details page. Obviously the 'interested' state in the Context Provider is to broad and changing all of them. How can I filter out the state change?

Here is the card component that gets rendered from a flatlist:

import React, { useState, useEffect, useContext } from 'react';
import { Image, View, Text, TouchableOpacity } from 'react-native';
import { localDate } from 'utils/formatDate';
import PropTypes from 'prop-types';
import { ConcertContext } from '../../../context/ConcertContextProvider';
import LocationIcon from '../../../assets/images/locationIcon.svg';
import InterestedFilledIcon from '../../../assets/images/interested-filled.svg';
import InterestedEmptyIcon from '../../../assets/images/interested-empty.svg';
import ShareIcon from '../../../assets/images/share.svg';
import styles from './styles';

export default function ConcertCard({
  gotoConcertPage,
  artist,
  cover,
  concertCity,
  scheduledAt,
  description,
  concertObj,
}) {
  const [clickInterest, setClickInterest] = useState(concertObj.is_interested);
  const { interested, setInterested, concertInterest } = useContext(ConcertContext);

  const handleInterest = () => {
    setInterested(prevState => !prevState);
    concertInterest(concertObj);
  };

  useEffect(() => {
    setClickInterest(interested);
  }, [interested]);

  return (
    <TouchableOpacity onPress={() => gotoConcertPage(concertObj)}>
      <View style={styles.card}>
        <View style={styles.cardContent}>
          <View style={styles.cropped}>
            <Image style={styles.image} source={{ url: cover.url }} />
          </View>
          <View style={styles.textWrapper}>
            <Text style={styles.date}>{localDate(scheduledAt, 'MMM D, h:mm A z')}</Text>
            <View style={styles.locationWrapper}>
              <LocationIcon style={styles.locationIcon} />
              <Text style={styles.location}>{concertCity.name}</Text>
              <Text style={styles.location}>{concertCity.address}</Text>
            </View>
            <Text style={styles.name}>{artist.name}</Text>
            <Text style={styles.description}>{description}</Text>
          </View>
          <View style={styles.border} />
        </View>
        <View style={styles.icons}>
          <View style={styles.sharedIcon}>
            <ShareIcon />
          </View>
          {!clickInterest ? (
            <InterestedEmptyIcon onPress={handleInterest} />
          ) : (
            <InterestedFilledIcon onPress={handleInterest} />
          )}
        </View>
      </View>
    </TouchableOpacity>
  );
}

ConcertCard.propTypes = {
  gotoConcertPage: PropTypes.func,
  artist: PropTypes.object,
  cover: PropTypes.object,
  concertCity: PropTypes.object,
  scheduledAt: PropTypes.string,
  description: PropTypes.string,
  concertObj: PropTypes.object,
};

Here is the card details page:

import React, { useState, useEffect, useContext } from 'react';
import { Text, View, Image, ImageBackground, ScrollView, FlatList } from 'react-native';
import PropTypes from 'prop-types';
import Button from 'components/button';
import { localDate } from 'utils/formatDate';
import { ConcertContext } from '../../context/ConcertContextProvider';
import LocationIcon from '../../assets/images/locationIconTwo.svg';
import LittleFriendIcon from '../../assets/images/little-friend.svg';
import Star from '../../assets/images/Star.svg';
import QuestionMarkIcon from '../../assets/images/question-mark.svg';
import ShareIcon from '../../assets/images/ShareIconLarge.svg';

import styles from './concertStyles';

export default function ConcertPageScreen({ navigation, route }) {
  const {
    concertObj,
    name,
    artist,
    scheduled_at,
    interests,
    concert_city,
    other_cities,
    description,
    ticket_base_price,
  } = route.params;

  const { interested, setInterested, concertInterest } = useContext(ConcertContext);

  const formatLocation = city => {
    return city.slice(0, city.length - 5);
  };

  const handleInterest = () => {
    setInterested(prevState => !prevState);
    concertInterest(concertObj);
  };

  const buyTicket = concertObj => {
    console.log('Buy ticked: ', concertObj.id);
  };

  return (
    <>
      <View style={styles.wrapper}>
        <ImageBackground
          style={styles.imageBackground}
          imageStyle={{ opacity: 0.3 }}
          source={{ url: artist.media }}
        />
        <ScrollView vertical>
          <View style={styles.headerWrapper}>
            <Text style={styles.headerTitle}>{name}</Text>
            <Text style={styles.smallText}>{artist.name}</Text>
            <Text style={styles.date}>{localDate(scheduled_at, 'MMM D, h:mm A z')}</Text>
            <View style={styles.locationWrapper}>
              <LocationIcon style={styles.locationIcon} />

              <Text style={styles.location}>{concert_city.name}</Text>
              <Text style={styles.location}>{concert_city.address}</Text>
            </View>
            <Text style={styles.description}>{description}</Text>
            <View style={styles.interestedWrapper}>
              <LittleFriendIcon style={styles.littleMan} />
              <Text style={styles.interested}>{interests} Interested</Text>
            </View>
            <View
              style={{
                borderTopColor: '#DADADA',
                borderTopWidth: 0.2,
                borderStyle: 'solid',
                alignSelf: 'center',
                height: 5,
                marginTop: 32,
                marginBottom: 28.5,
              }}
            />
            <View style={styles.tableWrapper}>
              <View style={styles.tableHeaderWrapper}>
                <Text style={styles.headerTitle}>DATES</Text>
                <Text style={styles.headerTitle}>LOCATIONS</Text>
              </View>
              <View>
                <FlatList
                  data={other_cities}
                  initialNumToRender={1}
                  renderItem={({ item }) => (
                    <View style={styles.tableItemsWrapper}>
                      <Text style={styles.dateItem}>
                        {localDate(item.date, 'MMM DD - h:mm A z')}
                      </Text>
                      <Text style={styles.locationItem}>{formatLocation(item.city)}</Text>
                    </View>
                  )}
                  concertCityId={item => `item${item.concert_city_id}`}
                  concertScheduleId={item => `item${item.concert_schedule_id}`}
                />
              </View>
            </View>
            <View style={styles.iconWrapper}>
              <View style={{ backgroundColor: '#292929', width: 44, height: 44, borderRadius: 30 }}>
                <ShareIcon style={styles.shareIcon} />
              </View>

              {!interested ? (
                <View
                  style={{ backgroundColor: '#292929', width: 44, height: 44, borderRadius: 30 }}>
                  <Star style={styles.starIcon} onPress={handleInterest} />
                </View>
              ) : (
                <View
                  style={{ backgroundColor: '#007AFF', width: 44, height: 44, borderRadius: 30 }}>
                  <Star style={styles.starIcon} onPress={handleInterest} />
                </View>
              )}
              <Image style={styles.roundAvatar} source={{ url: artist.media }} />
            </View>
            <View style={styles.iconWrapper}>
              <Text style={styles.shareIconText}>Share</Text>
              <Text style={styles.interestedIconText}>I'm Interested</Text>
              <Text style={styles.artistIconText}>Add Artist</Text>
            </View>
            <View style={styles.ticketWrapper}>
              <Text style={styles.ticketPrice}>TICKET PRICES</Text>
              <QuestionMarkIcon style={styles.ticketPrice} />
            </View>
            <View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
              <Text style={styles.ticketLocation}>In-location</Text>
              <Text style={styles.ticketLocation}>$ {ticket_base_price}</Text>
            </View>
            <View
              style={{
                borderTopColor: '#DADADA',
                borderTopWidth: 0.5,
                borderStyle: 'solid',
                height: 20,
                marginTop: 32,
                marginBottom: 28.5,
              }}
            />
            <View style={styles.footerWrapper}>
              <Text style={styles.headerTitle}>TICKET GUIDE</Text>
              <Text style={styles.ticketDescription}>
                This ticket is for a livestream concert. Sign In at www.colortv.me on your browser
                to watch on our TV or laptop. On the go? Watch from the COLOR TV mobile app.
              </Text>
            </View>
          </View>
          <Button
            rightIcon={false}
            text="Buy Ticket"
            style={styles.button}
            onPress={() => buyTicket(concertObj)}
          />
        </ScrollView>
      </View>
    </>
  );
}

ConcertPageScreen.propTypes = {
  navigation: PropTypes.object,
  route: PropTypes.object,
};

and here is the Context Provider:

import React, { createContext, useState } from 'react';
import { concertInterestApi } from 'utils/apiRoutes';
import useFetcher from 'hooks/useFetcher';
import parseError from '../utils/parseError';

export const ConcertContext = createContext(null);

const ConcertContextProvider = ({ children }) => {
  const { isLoading, error, fetcher } = useFetcher();
  const [interested, setInterested] = useState([]);

  const concertInterest = async concertObj => {
    try {
      await fetcher({
        url: concertInterestApi(concertObj.id),
        method: concertObj.is_interested ? 'DELETE' : 'POST',
      });
    } catch ({ response }) {
      throw parseError(response);
    }
  };

  return (
    <ConcertContext.Provider value={{ interested, setInterested, concertInterest }}>
      {children}
    </ConcertContext.Provider>
  );
};

export default ConcertContextProvider;

Perhaps I'm not wiring this up correctly at all and any suggestions would be welcome to manage state.

Demian Sims
  • 871
  • 1
  • 14
  • 29

1 Answers1

0

You are confused about the shape of your own context. In your Provider, interested is an array (I'm not sure of what) and setInterested is a function to replace that array. With that in mind, hopefully you can see the problem with this:

  const { interested, setInterested, concertInterest } = useContext(ConcertContext);

  const handleInterest = () => {
    setInterested(prevState => !prevState);
    concertInterest(concertObj);
  };

You are treating interested as a boolean when it is an array. I think it's all of the concerts that this user is interested it? Or perhaps an array of all userIds who are interested in this concert? Or perhaps useState([]) is a mistake and it was always meant to be a boolean?

Either way, I would recommend moving the shared logic from the ConcertCard and ConcertPageScreen into a custom hook that consumes your context and return an onClickInterested handler function. Your hook can take the concert id and/or user id as an argument.

Linda Paiste
  • 38,446
  • 6
  • 64
  • 102