skip to Main Content

Hej, I advanced my FlatList in React Native with
a) inbox/archive views
and b) with standard filter functionalities.

It’s working somehow, but is not production ready!
Can someone please check this (I think well-organized) code and tell me where I do what wrong?

What is not working:
a) FlatList does not always re-render/update when the stream state, which is its data prop, changes
b) FlatList does not remove an item immediately when I archive/unarchive via swipe functionality. I have to manually change the view to see …
c) FlatList does not directly apply the filter on state, I have to click twice to make it happen …

//React
import { View, StyleSheet, Pressable, Animated, FlatList } from "react-native";
import { useCallback, useContext, useEffect, useState, useMemo } from "react";

//Internal
import SelectBtn from "./SelectBtn";
import SessionComponent from "./SessionComponent";
import LoadingOverlay from "../notification/LoadingOverlay";
import { SessionsContext } from "../../store/context-reducer/sessionsContext";

//External
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from "@react-navigation/native";
import { database, auth } from "../../firebase";
import { ref, onValue, remove, update } from "firebase/database";

function SessionStream() {
  const navigation = useNavigation();

  const sessionsCtx = useContext(SessionsContext);
  const currentSessions = sessionsCtx.sessions;

  const [isFetching, setIsFetching] = useState(true);

  const [stream, setStream] = useState([]);
  const [inbox, setInbox] = useState([]);
  const [archive, setArchive] = useState([]);
  const [filter, setFilter] = useState([]);

  const sessionList = ["Sessions", "Archive"];
  const sortList = ["ABC", "CBA", "Latest Date", "Earliest Date"];

  useEffect(() => {
    //Fetches all sessions from the database
    async function getSessions() {
      setIsFetching(true);
      const uid = auth.currentUser.uid;
      const sessionsRef = ref(database, "users/" + uid + "/sessions/");
      try {
        onValue(sessionsRef, async (snapshot) => {
          const response = await snapshot.val();
          if (response !== null) {
            const responseObj = Object.entries(response);
            const sessionsData = responseObj.map((item) => {
              return {
                id: item[1].id,
                title: item[1].title,
                laps: item[1].laps,
                endTime: item[1].endTime,
                note: item[1].note,
                identifier: item[1].identifier,
                date: item[1].date,
                smed: item[1].smed,
                externalRatio: item[1].externalRatio,
                internalRatio: item[1].internalRatio,
                untrackedRatio: item[1].untrackedRatio,
                archived: item[1].archived,
              };
            });
            sessionsCtx.setSession(sessionsData);
            setIsFetching(false);
          } else {
            sessionsCtx.setSession([]);
            setIsFetching(false);
          }
        });
      } catch (err) {
        alert(err.message);
        setIsFetching(false);
      }
    }
    getSessions();
  }, []);

  useEffect(() => {
    //Sorts sessions into archived and unarchived
    setInbox(
      currentSessions.filter((session) => {
        return session.archived === false || session.archived === undefined;
      })
    );
    setArchive(
      currentSessions.filter((session) => {
        return session.archived === true;
      })
    );
  }, [currentSessions, archiveHandler, unArchiveHandler, sessionsCtx, stream]);

  if (isFetching) {
    setTimeout(() => {
      return <LoadingOverlay />;
    }, 5000);
  }

  const onPressHandler = useCallback(
    //Callback to open the session
    (item) => {
      navigation.navigate("Detail", {
        sessionID: item.id,
      });
    },
    [onPressHandler]
  );

  const rightSwipeActions = useCallback(
    //Swipe actions for the session list
    (item, swipeAnimatedValue) => {
      return (
        <View
          style={{
            flexDirection: "row",
            width: 168,
            height: 132,
          }}
        >
          {item.archived === false ? (
            <Pressable
              onPress={archiveHandler.bind(this, item)}
              style={({ pressed }) => pressed && styles.swipePressed}
            >
              <View style={styles.archive}>
                <Animated.View
                  style={[
                    styles.archive,
                    {
                      transform: [
                        {
                          scale: swipeAnimatedValue.interpolate({
                            inputRange: [0, 1],
                            outputRange: [0, 1],
                            extrapolate: "clamp",
                          }),
                        },
                      ],
                    },
                  ]}
                >
                  <Ionicons
                    name="ios-archive-outline"
                    size={24}
                    color="white"
                  />
                </Animated.View>
              </View>
            </Pressable>
          ) : (
            <Pressable
              onPress={unArchiveHandler.bind(this, item)}
              style={({ pressed }) => pressed && styles.swipePressed}
            >
              <View style={styles.unArchive}>
                <Animated.View
                  style={[
                    styles.unArchive,
                    {
                      transform: [
                        {
                          scale: swipeAnimatedValue.interpolate({
                            inputRange: [0, 1],
                            outputRange: [0, 1],
                            extrapolate: "clamp",
                          }),
                        },
                      ],
                    },
                  ]}
                >
                  <Ionicons
                    name="md-duplicate-outline"
                    size={24}
                    color="white"
                  />
                </Animated.View>
              </View>
            </Pressable>
          )}

          <Pressable
            onPress={deleteHandler.bind(this, item)}
            style={({ pressed }) => pressed && styles.pressed}
          >
            <View style={styles.trash}>
              <Animated.View
                style={[
                  styles.trash,
                  {
                    transform: [
                      {
                        scale: swipeAnimatedValue.interpolate({
                          inputRange: [0, 1],
                          outputRange: [0, 1],
                          extrapolate: "clamp",
                        }),
                      },
                    ],
                  },
                ]}
              >
                <Ionicons name="trash-outline" size={24} color="white" />
              </Animated.View>
            </View>
          </Pressable>
        </View>
      );
    },
    [rightSwipeActions]
  );

  const deleteHandler = useCallback(
    (item) => {
      try {
        sessionsCtx.deleteSession(item.id); // delete from local context
        const uid = auth.currentUser.uid;
        const sessionRef = ref(
          database,
          "users/" + uid + "/sessions/" + item.id
        );
        remove(sessionRef); // delete from firebase
      } catch (error) {
        alert(error.message);
      }
    },
    [deleteHandler]
  );

  const archiveHandler = (item) => {
    try {
      const id = item.id;
      const updatedSession = {
        ...item, // copy current session
        archived: true,
      };
      const uid = auth.currentUser.uid;
      const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
      update(sessionRef, updatedSession);
      /*  sessionsCtx.updateSession(id, updatedSession); */
      //update inbox state
      setInbox(
        currentSessions.filter((session) => {
          const updatedData = session.archived === false;
          return updatedData;
        })
      );
      //update archive state
      setArchive(
        currentSessions.filter((session) => {
          const updatedData = session.archived === true;
          return updatedData;
        })
      );
    } catch (error) {
      alert(error.message);
    }
  };

  const unArchiveHandler = (item) => {
    try {
      const id = item.id;
      const updatedSession = {
        ...item, // copy current session
        archived: false,
      };
      const uid = auth.currentUser.uid;
      const sessionRef = ref(database, "users/" + uid + "/sessions/" + id);
      update(sessionRef, updatedSession);
      /*    sessionsCtx.updateSession(id, updatedSession); */
      //update unarchived session list
      setArchive((preState) => {
        //remove the item from archived list
        preState.filter((session) => session.id !== item.id);
        return [...preState];
      });
    } catch (error) {
      alert(error.message);
    }
  };

  const selectSessionHandler = useCallback(
    (selectedItem) => {
      switch (selectedItem) {
        case "Sessions":
          setStream(inbox);
          break;
        case "Archive":
          setStream(archive);
          break;
      }
    },
    [selectSessionHandler, inbox, archive]
  );

  const selectFilterHandler = (selectedItem) => {
    //filter the session list
    switch (selectedItem) {
      case "ABC":
        // Use the Array.sort() method to sort the list alphabetically in ascending order
        const sortedList = stream.sort((a, b) => {
          return a.title.localeCompare(b.title);
        });
        setStream((preState) => {
          return [...sortedList];
        });
        break;
      case "CBA":
        // Use the Array.sort() method to sort the list alphabetically in descending order
        const sortedList2 = stream.sort((a, b) => {
          return b.title.localeCompare(a.title);
        });
        setStream((preState) => {
          return [...sortedList2];
        });
        break;
      case "Latest Date":
        // Use the Array.sort() method to sort the list by date in descending order
        const sortedList3 = stream.sort((a, b) => {
          return b.date.localeCompare(a.date);
        });
        setStream((preState) => {
          return [...sortedList3];
        });
        break;
      case "Earliest Date":
        // Use the Array.sort() method to sort the list by date in ascending order
        const sortedList4 = stream.sort((a, b) => {
          return a.date.localeCompare(b.date);
        });
        setStream((preState) => {
          return [...sortedList4];
        });
        break;
    }
  };

  const renderSessionItem = useCallback(({ item }) => {
    return (
      <Pressable
        /*         style={({ pressed }) => pressed && styles.pressed} */
        onPress={onPressHandler.bind(null, item)}
        key={item.id}
      >
        <SessionComponent
          key={item.id}
          title={item.title}
          identifier={item.identifier}
          date={item.date}
          rightSwipeActions={rightSwipeActions.bind(null, item)}
          smed={item.smed}
          endTime={item.endTime}
        />
      </Pressable>
    );
  }, []);

  return (
    <View style={styles.container}>
      <View style={styles.menuRow}>
        <SelectBtn
          data={sortList}
          onSelect={(item) => selectFilterHandler(item)}
        />
        <SelectBtn
          data={sessionList}
          onSelect={(item) => selectSessionHandler(item)}
        />
      </View>
      <FlatList
        data={stream}
        renderItem={renderSessionItem}
        keyExtractor={(item) => item.id}
        extraData={stream}
      />
    </View>
  );
}

export default SessionStream;

What I already tried:
I tried ChatGPT the whole day yesterday … 😉
I tried updating the global state for sessions to trigger re-renders …
I tried updating the local state as obj or via the spread operator to …
I tried extraData prop at FlatList
I removed useCallback to make sure it doesnt somehow block …

2

Answers


  1. can you do this when you are updating your stream state

      const oldStream = stream;
      const newStream = newStream; // put the value that you have updated
    
      const returnedTarget= Object.assign(stream, newStream);
      setStream(returnedTarget);
    

    The problem might be you are mutating the copy obj
    ref: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign

    Login or Signup to reply.
  2. c) in your selectFilterHandler method you are updating filter and in the same method you are trying to use updated filter value. which is not possible as useState does not update value immediately.

    b) in your archiveHandler i think you are not updating the setStream state. if you are trying to run

    const selectSessionHandler = useCallback(
        (selectedItem) => {
          switch (selectedItem) {
            case "Sessions":
              setStream(inbox);
              break;
            case "Archive":
              setStream(archive);
              break;
          }
        },
        [selectSessionHandler, inbox, archive]
      );
    

    this method whenever inbox/archive changes it will not work it will be work only when you call this method and any of the value in dependency array has changed. (you can use useEffect and pass and deps arr [inbox,archive] which will run every time and can update your state.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search