Search code examples
react-nativecachinggraphqlapolloapollo-client

UI not updating after adding or removing items with apollo cache and reactive variables


I'm having a hard time updating my UI after adding or removing items on my react native app with apollo cache.

To explain a little bit more. I have an explorer Screen where are displayed some items with a toggle to subscribe or unsubscribe to an item. To another screen called "Subscribe Screen", I'm supposed to displayed all my favorite items. So I created a reactive variable called allFavoritesVar where I can add or delete items from there.

So in my cache.js I have :

import {InMemoryCache} from '@apollo/client/core';
import {makeVar} from '@apollo/client';

export const allFavoritesVar = makeVar([]);

export const cache = new InMemoryCache({
  Query: {
    fields: {
      userFavorites: {
          read() {
            return allFavoritesVar();
          },
        },
    }
  }
})

So on my Explorer screen i'm checking if each item exist in allFavoritesVar to make the toggle red and inform the user that these items are already in their "Subscribe Screen".

const favExists = (flux) => {
    if (allFavoritesVar().filter((item) => item.id === flux.id).length > 0) {
      return true;
    }
    return false;
  }; 

I was using redux before and switch to apollo because I needed to persist the cache when users were opening their app. Everything was much simpler with redux, the toggle were working fine and becoming red or grey when adding or removing items from the store and the "subscribe screen" was updating itself.

Now when I'm toggling, the mutation works, I can see that items are added or removed, but my ui is not updating. And when I'm closing my app, the last state of the cache is not displayed.

Here is my Explorer Screen :

import React, {useEffect, useState} from 'react';
import {
  SafeAreaView,
  StyleSheet,
  Dimensions,
  ScrollView,
  TouchableOpacity,
  Image,
  FlatList,
  ActivityIndicator,
} from 'react-native';
import {
  NetworkStatus,
  useLazyQuery,
  useMutation,
  useQuery,
} from '@apollo/client';
import {useSelector, useDispatch} from 'react-redux';
import {Box, Text} from 'react-native-design-utility';
import {Notifier} from 'react-native-notifier';
import {useTheme} from '@react-navigation/native';
import ErrorIcon from 'react-native-vector-icons/Ionicons';
import RefreshIcon from 'react-native-vector-icons/Ionicons';
import {theme} from '../theme/theme';
import Loading from '../components/Loading';
import CustomNotifier from '../components/CustomNotifier';
import CustomNotifierError from '../components/CustomNotifierError';
import SubscribeItem from '../components/SubscribeItem';
import {
  SUBSCRIBE_FLUXGROUP_MUTATION,
  SUBSCRIBE_FLUX_MUTATION,
  UNSUBSCRIBE_FLUXGROUP_MUTATION,
  UNSUBSCRIBE_FLUX_MUTATION,
} from '../graphql/mutations/fluxMutations';
import {
  GET_EXPLORER_CATEGORIES_QUERY,
  GET_EXPLORER_SLIDES_QUERY,
} from '../graphql/queries/explorerQueries';
import ToggleIcon from '../components/ToggleIcon';
import {HEIGHT} from '../utils/constants';
import {ALL_FAVORITES_QUERY} from '../graphql/queries/userQueries';
import {allFavoritesVar, cache} from '../utils/cache';
import {FLUX_QUERY} from '../graphql/queries/fluxesQueries';

const WIDTH = Dimensions.get('window').width;
const PAGE_SIZE = 10;

const ExplorerScreen = ({navigation}) => {
  const {colors, dark} = useTheme();
  const [limit, setLimit] = useState(PAGE_SIZE);
  const [isError, setError] = useState('');
  const [isLoading, setIsLoading] = useState(false);
  const {
    data: explorerData,
    loading: explorerLoading,
    error,
    refetch,
  } = useQuery(GET_EXPLORER_CATEGORIES_QUERY, {
    fetchPolicy: 'cache-first',
    errorPolicy: 'all',
  });
  const {data: favoritesData, loading: favLoading} =
    useQuery(ALL_FAVORITES_QUERY);
  const {data: slidesData, loading: slidesLoading} = useQuery(
    GET_EXPLORER_SLIDES_QUERY,
    {
      fetchPolicy: 'cache-first',
    },
  );
  const [subscribeToFlux] = useMutation(SUBSCRIBE_FLUX_MUTATION);
  const [subscribeToFluxGroup] = useMutation(SUBSCRIBE_FLUXGROUP_MUTATION);
  const [unsubscribeFromFlux] = useMutation(UNSUBSCRIBE_FLUX_MUTATION);
  const [unsubscribeFromFluxGroup] = useMutation(
    UNSUBSCRIBE_FLUXGROUP_MUTATION,
  );

  const addFav = (flux) => {
    const explorerFav = allFavoritesVar([...allFavoritesVar(), flux]);
    console.log('explorerFav: ', explorerFav);
    return explorerFav;
  };

  const favExists = (flux) => {
    if (allFavoritesVar().filter((item) => item.id === flux.id).length > 0) {
      return true;
    }
    return false;
  };

  const handleAddFavorite = async (flux) => {
    if (flux.__typename === 'FluxGroup') {
      addFav(flux);
      Notifier.showNotification({
        title: 'Vous êtes abonné à ce groupe de flux',
        Component: CustomNotifier,
        componentProps: {
          alertType: 'info',
        },
      });
      await subscribeToFluxGroup({
        variables: {
          id: parseInt(flux.id),
          frequency: 'all',
        },
      });
    } else {
      addFav(flux);
      Notifier.showNotification({
        title: 'Vous êtes abonné à ce flux',
        Component: CustomNotifier,
        componentProps: {
          alertType: 'info',
        },
      });
      await subscribeToFlux({
        variables: {
          id: parseInt(flux.id),
          frequency: 'all',
        }
      });
    }
  };

  const handleRemoveFavorite = async (flux) => {
    if (flux.__typename === 'FluxGroup') {
      Notifier.showNotification({
        title: 'Vous êtes désabonné de ce groupe de flux',
        Component: CustomNotifierError,
        componentProps: {
          alertType: 'error',
        },
      });
      await unsubscribeFromFluxGroup({
        variables: {
          id: parseInt(flux.id),
        },
        update: (cache, {data}) => {
          const existingFavs = cache.readQuery({
            query: ALL_FAVORITES_QUERY,
          });
          //console.log('DATA UPDATE:', data);
          const newFavs = existingFavs.userFavorites.filter(
            (item) => item.id !== flux.id,
          );
          console.log('DATA UPDATE:', newFavs);
          cache.writeQuery({
            query: ALL_FAVORITES_QUERY,
            data: {userFavorites: [newFavs, ...existingFavs.userFavorites]},
          });
        },
      });
    } else {
      Notifier.showNotification({
        title: 'Vous êtes désabonné de ce flux',
        Component: CustomNotifierError,
        componentProps: {
          alertType: 'error',
        },
      });
      await unsubscribeFromFlux({
        variables: {
          id: parseInt(flux.id),
        },
        update: (cache, {data}) => {
          const existingFavs = cache.readQuery({
            query: ALL_FAVORITES_QUERY,
          });
          //console.log('DATA UPDATE:', data);
          const newFavs = existingFavs.userFavorites.filter(
            (item) => item.id !== flux.id,
          );
          console.log('DATA UPDATE:', newFavs);
          cache.writeQuery({
            query: ALL_FAVORITES_QUERY,
            data: {userFavorites: [newFavs, ...existingFavs.userFavorites]},
          });
        },
      });
    }
  };

  function sliceIntoChunks(arr, chunkSize) {
    const res = [];
    for (let i = 0; i < arr.length; i += chunkSize) {
      const chunk = arr.slice(i, i + chunkSize);
      res.push(chunk);
    }
    return res;
  }

  useEffect(() => {
    if (error) {
      setIsLoading(true);
      setError(error.message);
      setIsLoading(false);
    }
  }, [error]);

  const SeeMore = ({onPress}) => {
    return (
      <TouchableOpacity onPress={onPress}>
        <Text
          size={15}
          mr="sm"
          color={dark ? 'primary' : colors.text}
          style={styles.letSpacing}>
          Tout Voir
        </Text>
      </TouchableOpacity>
    );
  };

  const renderHeader = () => {
    if (slidesLoading) {
      return (
        <ScrollView
          horizontal
          showsHorizontalScrollIndicator={false}
          contentContainerStyle={{
            paddingHorizontal: theme.space.sm,
            paddingTop: theme.space.sm,
            height: HEIGHT / 4.8,
            justifyContent: 'center',
            alignItems: 'center',
            width: WIDTH,
          }}>
          <ActivityIndicator color={theme.color.primary} size={24} />
        </ScrollView>
      );
    }
    return (
      <>
        <ScrollView
          horizontal
          showsHorizontalScrollIndicator={false}
          contentContainerStyle={{
            paddingHorizontal: theme.space.sm,
            paddingTop: theme.space.sm,
            height: HEIGHT / 4.8,
          }}>
          {slidesData.explorer_slides.map((slide) => {
            const type = slide.item.__typename;
            return (
              <TouchableOpacity
                key={slide.id}
                onPress={() =>
                  navigation.navigate(
                    type === 'Flux'
                      ? 'SingleFlux'
                      : type === 'FluxGroup'
                      ? 'MultipleFlux'
                      : 'FluxCategory',
                    {
                      headerTitle: slide.item.name,
                      headerItem: slide.item,
                      itemId: slide.item.id,
                      headerText:
                        slide.item.__typename !== 'FluxCategory'
                          ? slide.item.description
                          : null,
                    },
                  )
                }>
                <Box
                  mx="xs"
                  bg="primary"
                  w={WIDTH - 120}
                  h={150}
                  radius="sm"
                  align="center"
                  justify="center"
                  overflow="hidden">
                  <Image
                    source={{uri: slide.image.uri}}
                    style={styles.imgCat}
                    resizeMode="cover"
                  />
                </Box>
              </TouchableOpacity>
            );
          })}
        </ScrollView>
        <Box mt="md" h={1} w={WIDTH} bg={dark ? 'grey' : 'lightBorder'} />
      </>
    );
  };

  const renderItem = ({item, index}) => {
    return (
      <Box key={item - index} mb={8}>
        {item.map((section, index) => {
          const multiple = section.__typename === 'FluxGroup';
          const subscribed = section.subscribed;
          return (
            <TouchableOpacity
              key={section.id}
              onPress={() =>
                !multiple
                  ? navigation.navigate('SingleFlux', {
                      headerTitle: section.name,
                      itemId: section.id,
                      headerItem: section,
                      subscribed: subscribed,
                      itemExist: exists(section),
                    })
                  : navigation.navigate('MultipleFlux', {
                      headerTitle: section.name,
                      itemId: section.id,
                      headerItem: section,
                      subscribed: subscribed,
                      itemExist: exists(section),
                    })
              }>
              <SubscribeItem
                flux={section}
                id={section.id}
                channel={section.name}
                title={
                  section.description
                    ? section.description
                    : `Toutes les actualités sur ${section.name}`
                }
                icon={section.image?.uri ? `${section.image?.uri}` : null}
                custom={section.customChannel}
                pushNumber={section.frequency_numbers_all}
                multiple={multiple}
                button={
                  <>
                    {/* <ToggleIcon
                    favorite={exists(section)}
                    onPress={() =>
                      exists(section)
                        ? handleRemoveFavorite(section)
                        : handleAddFavorite(section)
                    }
                  /> */}
                    <ToggleIcon
                      favorite={favExists(section)}
                      onPress={() =>
                        favExists(section)
                          ? handleRemoveFavorite(section)
                          : handleAddFavorite(section)
                      }
                    />
                  </>
                }
              />
            </TouchableOpacity>
          );
        })}
      </Box>
    );
  };

  const renderCategories = () => {
    if (!explorerData) {
      return (
        <Box py="sm">
          <Text mb="sm" center color="lightGrey">
            Catégories en chargement
          </Text>
          <Loading />
        </Box>
      );
    }
    if (explorerData) {
      return explorerData.explorer_categories.map((section) => {
        const sectionData = sliceIntoChunks(section.related, 3);
        return (
          <>
            <Box
              w={WIDTH}
              key={section.id}
              dir="row"
              justify="between"
              align="center">
              <Text
                size="xl"
                pt="sm"
                pb="2xs"
                ml="sm"
                color={dark ? 'white' : 'black'}
                style={styles.header}>
                {section.name}
              </Text>
              <SeeMore
                onPress={() =>
                  navigation.navigate('FluxCategory', {
                    headerTitle: section.name,
                    headerItem: section,
                    itemId: section.id,
                    headerText: null,
                  })
                }
              />
            </Box>
            <Box>
              <FlatList
                horizontal
                pagingEnabled={true}
                showsHorizontalScrollIndicator={false}
                contentContainerStyle={styles.contentContainerStyle}
                data={section ? sectionData : []}
                renderItem={renderItem}
                extraData={favoritesData}
                keyExtractor={(item, index) => item + index}
                onEndReachedThreshold={0}
              />
              <Box h={1} bg={dark ? 'grey' : 'lightBorder'} mb="sm" />
            </Box>
          </>
        );
      });
    }
  };

  if (error) {
    return (
      <Box f={1} justify="center" align="center">
        <Box mb="xs">
          <ErrorIcon
            name="cloud-offline-outline"
            color={dark ? theme.color.lightGrey : 'grey'}
            size={32}
          />
        </Box>
        <Text
          size="md"
          center
          color={dark ? 'lightGrey' : 'grey'}
          style={styles.letSpacing}>
          Une erreur s'est produite
        </Text>
        <Text
          size="sm"
          color={dark ? 'lightGrey' : 'grey'}
          style={styles.letSpacing}>
          Réessayez plus tard
        </Text>
        <TouchableOpacity onPress={() => refetch()}>
          <Box mt="sm">
            <RefreshIcon name="refresh" size={24} color={theme.color.primary} />
          </Box>
        </TouchableOpacity>
      </Box>
    );
  }

  if (isLoading) {
    return <Loading />;
  }

  return (
    <SafeAreaView
      style={[styles.container, {backgroundColor: colors.background}]}>
      <ScrollView showsVerticalScrollIndicator={false}>
        <Box>{renderHeader()}</Box>
        {renderCategories()}
      </ScrollView>
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    width: WIDTH,
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  searchBar: {
    width: WIDTH,
    backgroundColor: theme.color.secondary,
    borderBottomColor: theme.color.secondary,
    borderTopColor: theme.color.secondary,
  },
  inputBar: {
    backgroundColor: theme.color.black,
    borderRadius: theme.space.md,
  },
  header: {
    fontFamily: 'System',
    fontWeight: '700',
    letterSpacing: 0,
  },
  icon: {
    width: 25,
    height: 25,
    borderRadius: 6,
    backgroundColor: theme.color.primary,
    overflow: 'hidden',
  },
  iconNull: {
    width: 25,
    height: 25,
    borderRadius: 6,
    backgroundColor: theme.color.primary,
    overflow: 'hidden',
  },
  imgCat: {
    width: '100%',
    height: 150,
  },
  letSpacing: {
    letterSpacing: 0,
  },
});
export default ExplorerScreen;

Am I missing something ? Or Am I doing it totally wrong haha ? If you need more info on my code, don't hesitate to ask :)


Solution

  • Well turns out, you can't persist a reactive variable! So I'm just refetching queries and updating my cache after a mutation :) All good now ! Thank you !