javascriptfirebasegoogle-cloud-firestorereactfire

Reactfire not showing live updates made in Firestore


I'm trying to get my head around Firestore/Reactfire so am working on a basic todo app to help me grasp the concepts.

I have a screen /Screens/Mainscreen.js that loads all of a user's todo lists. There's an input at the top and a button to add the new list. This all works, except the new list isn't loaded onto the screen? If I refresh the screen, the new list is loaded then. It seems that I've missed some part of how Reactfire works, but my code seems to follow all of the demo docs so I'm not sure what I'm doing wrong.

Full WIP project here: https://github.com/warm--tape/todo/ (add a .env with Firebase credentials in if desired)

Here's my MainScreen.js:

function MainScreen({ navigation }) {

  // Logout action. Probably move.
  const auth = useAuth();
  function handleLogOut() {
    auth.signOut().then(() => navigation.replace('AuthScreen'));
  }

  // Load User
  const { status: userStatus, data: user } = useUser();
  const { uid } = user;

  // Load User Lists from Firestore
  const firestore = useFirestore();
  const listCollection = collection(firestore, 'lists');
  const userListQuery = query(listCollection, where('access', 'array-contains', uid || 0));
  const { status: listStatus, data: rawListData } = useFirestoreCollectionData(userListQuery, { idField: 'id' });

  // Load toast
  const toast = useToast();

  // Set up state  
  const [isLoading, setIsLoading] = useState(true);
  const [listData, setListData] = useState([]);

  const [listToAdd, setListToAdd] = useState({ listName: '' });
  const [itemToAdd, setItemToAdd] = useState({ itemName: '' });
  const [errors, setErrors] = useState({});

  // Hide screen until loaded
  if (isLoading && userStatus === 'success' && listStatus === 'success') {
    setListData(rawListData);
    setIsLoading(false);
  }

  // Form Validator
  const validate = () => {
    // Currently no validation
    return true;
  };

  // Handle add list
  async function onAddList() {
    if (validate()) {
      delete listToAdd.NO_ID_FIELD;
      const listToAddData = { ...listToAdd, ...{ owner: uid, access: [uid] } };
      await addDoc(collection(firestore, "lists"), listToAddData).then(() => {
        toast.show({
          title: "List Added",
          placement: "bottom"
        });
        setListToAdd({ listName: '' });
      });
    } else {
      alert('Validation Failed')
    }
  };

  // Handle press on list
  function onPressListHandler(id) {
    navigation.navigate('ListDetailScreen', {listId: id})
  }

  // =========================================================================
  // Render loading spinner

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

  // =========================================================================
  // Render

  const listRenderItem = ({ item }) => (
    <Pressable
      onPress={()=>{onPressListHandler(item.id)}}
      borderBottomWidth="1" _dark={{borderColor: "gray.600"}} borderColor="coolGray.200" pl="4" pr="5" py="2"
    >
      <HStack space={3} justifyContent="start" alignItems="center">
        <IconButton variant="unstyled" icon={<Icon as={Ionicons} name="list-outline" size="sm" />} onPress={() => {}} />
        <Text _dark={{color: "warmGray.50"}} color="coolGray.800">
          {item.listName}
        </Text>
      </HStack>
    </Pressable>
  );


  // =========================================================================
  // Render

  return (
    <ScreenWrapper>

      <HStack>
        <FormControl isRequired isInvalid={'listName' in errors}>
          <Input placeholder="Add List..." value={listToAdd.listName} onChangeText={value => setListToAdd({ ...listToAdd, ...{ listName: value } })} />
          {'listName' in errors ? <FormControl.ErrorMessage>{errors.listName}</FormControl.ErrorMessage> : null}
        </FormControl>
        <Button onPress={() => { onAddList() }} >
          <Icon color="white" as={Ionicons} name="add" size="sm" />
        </Button>
      </HStack>

      <Divider my="3" />

      <Box>
        <FlatList
          data={listData}
          renderItem={listRenderItem}
          keyExtractor={item => item.id} />
      </Box>

      <Divider my="3" />

      <Button onPress={() => handleLogOut()}>Logout</Button>

    </ScreenWrapper>
  );
}

export default MainScreen;

Solution

  • Turns out I had structured my code badly, so after reviewing a few demos and the docs in depth, the following refactor works:

    import React, { useState } from 'react';
    import { useFirestore, useUser, useFirestoreCollectionData } from 'reactfire';
    import { addDoc, collection, query, where } from 'firebase/firestore';
    
    import {
      Text,
      Button,
      FlatList,
      HStack,
      Pressable,
      Icon,
      FormControl,
      Input,
      IconButton,
      useToast,
      Divider
    } from 'native-base';
    import { Ionicons } from '@expo/vector-icons';
    import ScreenWrapper from '../components/ScreenWrapper';
    import LoadingSpinner from '../components/LoadingSpinner';
    
    function onPressListHandler(id) {
      navigation.navigate('ListDetailScreen', { listId: id })
    }
    
    const listRenderItem = ({ item }) => (
      <Pressable
        onPress={() => { onPressListHandler(item.id) }}
        borderBottomWidth="1" _dark={{ borderColor: "gray.600" }} borderColor="coolGray.200" pl="4" pr="5" py="2"
      >
        <HStack space={3} justifyContent="start" alignItems="center">
          <IconButton variant="unstyled" icon={<Icon as={Ionicons} name="list-outline" size="sm" />} onPress={() => { }} />
          <Text _dark={{ color: "warmGray.50" }} color="coolGray.800">
            {item.listName}
          </Text>
        </HStack>
      </Pressable>
    );
    
    const ListsList = () => {
    
      // Load User
      const { status: userStatus, data: user } = useUser();
      const { uid } = user;
    
      // Load User Lists from Firestore
      const firestore = useFirestore();
      const listCollection = collection(firestore, 'lists');
      const userListQuery = query(listCollection, where('access', 'array-contains', uid || 0));
      const { status: listStatus, data: lists } = useFirestoreCollectionData(userListQuery, { idField: 'id' });
    
      // Loading lists
      if (userStatus === 'loading' || listStatus === 'loading') {
        return <LoadingSpinner />;
      }
    
      // Render
      return (
        <>
          <FlatList
            data={lists}
            renderItem={listRenderItem}
            keyExtractor={item => item.id}
          />
        </>
      );
    };
    
    function MainScreen({ navigation }) {
    
      // Load toast
      const toast = useToast();
    
      const [listToAdd, setListToAdd] = useState({ listName: '' });
      const [errors, setErrors] = useState({});
    
      // Form Validator
      const validate = () => {
        // None needed here.
        return true;
      };
    
      // Handle add list
      async function onAddList() {
        if (validate()) {
          delete listToAdd.NO_ID_FIELD;
          const listToAddData = { ...listToAdd, ...{ owner: uid, access: [uid] } };
          await addDoc(collection(firestore, "lists"), listToAddData).then(() => {
            toast.show({
              title: "List Added",
              placement: "bottom"
            });
            setListToAdd({ listName: '' });
          });
        } else {
          alert('Validation Failed')
        }
      };
    
    
      // =========================================================================
      // Render
    
      return (
        <ScreenWrapper>
    
          <HStack>
            <FormControl isRequired isInvalid={'listName' in errors}>
              <Input placeholder="Add List..." value={listToAdd.listName} onChangeText={value => setListToAdd({ ...listToAdd, ...{ listName: value } })} />
              {'listName' in errors ? <FormControl.ErrorMessage>{errors.listName}</FormControl.ErrorMessage> : null}
            </FormControl>
            <Button onPress={() => { onAddList() }} >
              <Icon color="white" as={Ionicons} name="add" size="sm" />
            </Button>
          </HStack>
    
          <Divider my="3" />
    
          <ListsList />
    
        </ScreenWrapper>
      );
    }
    
    export default MainScreen;