1

I'm building a to do list app as part of a coding course, using Firebase Realtime Database and React Native with Expo.

enter image description here

I have no problems rendering the to do list, and in this case clicking a checkbox to indicate whether the task is prioritized or not.

However, each time I click on the checkbox to change the priority of a single task in the to do list, the entire Flatlist re-renders.

Each task object is as follows:

{id: ***, text: ***, priority: ***}

Task Component: (It consists of the text of the to do (task.text), and also a checkbox to indicate whether the task is prioritized or not). I've wrapped this component in React.memo, and the only props passed down from Todolist to Task are the individual task, but it still re-renders every time. (I left out most of the standard imports in the code snippet below)

import { CheckBox } from '@rneui/themed';

const Task = ({ 
    item, 
    }) => {

    console.log(item)
    
    const { user } = useContext(AuthContext);

    const onPressPriority = async () => {
            await update(ref(database, `users/${user}/tasks/${item.id}`), {
                priority: !item.priority,
            });
        };


    return (
        <View
            style={{ flexDirection: 'row', alignItems: 'center', width: '95%' }}
        >
            <View
                style={{ width: '90%' }}
            >
                <Text>{item.text}</Text>
            </View>
            <View
                style={{ width: '10%' }}
            >
                <CheckBox
                    checked={item.priority}
                    checkedColor="#00a152"
                    iconType="material-community"
                    checkedIcon="checkbox-marked"
                    uncheckedIcon={'checkbox-blank-outline'}
                    onPress={onPressPriority}
                />
            </View>
        </View>
        )}


        export default memo(Task, (prevProps, nextProps) => { 
            if (prevProps.item !== nextProps.item) {
                return true
            }
            return false
        })

To Do List parent component: Contains the Flatlist which renders a list of the Task components. It also contains a useEffect to update the tasks state based on changes to the Firebase database, and the function (memoizedOnPressPriority) to update the task.priority value when the Checkbox in the task component is clicked. Since memoizedOnPressPriority is passed a prop to , I've tried to place it in a useCallback, but is still re-rendering all items when the checkbox is clicked. (I left out most of the standard imports in the code snippet below)

export default function Home2() {

  const { user } = useContext(AuthContext);
  const [tasks, setTasks] = useState([]);

  useEffect(() => {
    if (user) {
      return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
        const todos = snapshot.val();
        const tasksCopy = [];
        for (let id in todos) {
          tasksCopy.push({ ...todos[id], id: id });
        }

        setTasks(tasksCopy);
      });
    } else {
      setTasks([]);
    }
  }, [user]);


  const renderItem = ({ item }) => (
    <TaskTwo
      item={item}
    />
  );


  return (
    <View style={styles.container}>
          <FlatList
            data={tasks}
            initialNumToRender={5}
            windowSize={4}
            renderItem={renderItem}
            keyExtractor={item => item.id}
          />
    </View>
  );
}

Could anyone let me know what I'm doing wrong, and how I can prevent the entire Flatlist from re-rendering each time I invoke the memoizedOnPressPriority function passed down to the Task component from the TodoList parent component? Any help is much appreciated!

The flamegraph for the render is below:

enter image description here

Update: I moved the prioritize function (memoizedOnPressPriority) into the Task component and removed the useCallback - so it's not being passed as a prop anymore. The re-render still happens whenever I press it.

Update 2: I added a key extractor , and also a custom equality function into the memoized task component. Still keeps rendering!

Lloyd Rajoo
  • 137
  • 2
  • 13

1 Answers1

1

I'm not familiar with Firebase Realtime Database, but if I understand the logic correctly, the whole tasks array is updated when one item changes, and this is what is triggering the list update.

Fixing the memo function

Wrapping the Task component in memo does not work because it performs a shallow comparison of the objects. The objects change each time the data is updated because a new tasks array with new objects is created, so the references of the objects are different. See this post for more details.

To use memo, we have to pass a custom equality check function, that returns true if the component is the same with new props, like so:

export default memo(Task, (prevProps, nextProps) => { 
  if (prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) {
    return true;
  }
  return false;
})

Note that is the text is modifiable, you'll want to check that too.

Alternative solution : read data from the Task component

This solution is recommended and takes full advantage of Firebase Realtime Database. To update only the component that is updated, you need to pass an array of ids to your flatlist, and delegate the data reading to the child component.

It's a pattern I use with redux very often when I want to update a component without updating the whole flatlist.

I checked the documentation of Firebase Realtime Database, and they indeed encourage you to read data at the lowest level. If you have a large list with many properties, it's not performant to receive the whole list when only one item is updated. Under the hood, the front-end library manages the cache system automatically.

//TodoList parent Component
...
const [tasksIds, setTasksIds] = useState([]);

useEffect(() => {
    if (user) {
      return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
        const todos = snapshot.val();
        // Build an array of ids
        const tasksIdsFromDb = todos.map((todo) => todo.id);
        setTasksIds(tasksCopy);
      });
    } else {
      setTasksIds([]);
    }
  }, [user]);

...
// keep the rest of the code and pass tasksIds instead of tasks to the flatlist
const Task = ({ taskId, memoizedOnPressPriority }) => {
  const [task, setTask] = useState(null)

  const { user } = useContext(AuthContext);

  useEffect(() => {
    if (user) {
      // retrieve data by id so only the updated component will rerender
      // I guess this will be something like this
      return onValue(ref(database, `users/${user}/tasks/${taskId}`), (snapshot) => {
        const todo = snapshot.val();
        setTask(todo);
      });
    } else {
      setTask(null);
    }
  }, [user]);

if (task === null) {
  return null
}

// return the component like before
Camille Hg
  • 196
  • 1
  • 6
  • Thanks! The entire tasks array is listed in the tasks state - I'm not passing that as a prop into the Task component and I'm wrapping Task in memo - I understood this to mean that Task won't re render unless there is a change to its props. Each task also has an id currently so I didn't specifically specify one with keyextractor. Should that prevent it from re-rendering? – Lloyd Rajoo Jan 14 '23 at 19:14
  • 1
    You are still passing tasks to the Flatlist in `data={tasks}`, so when tasks is updated the list rerenders. What the Flatlist does is basically creating a Task for each entry in your array, so it makes sense that the list rerenders each time tasks changes. KeyExtractor won't change this. To sum up, if your Flatlist and the only updated component rerender, the memoization works as expected. If the memoization fails all components will rerender (Flatlist + updated Todo + non updated Todos). In which case are you? – Camille Hg Jan 14 '23 at 19:49
  • It seems like all my components are re-rendering (Flatlist + updated Todo + non updated Todos - that's what I can tell from the flamegraph). I think you're right, it does seem like my memoization is failing. But I can't figure out to do besides what I'm currently doing (wrapping the individual task components in React.memo, and memoizing the function that is passed down into a usecallback). – Lloyd Rajoo Jan 14 '23 at 19:56
  • 1
    Ok, I wonder if moving the onPress function inside the Task component would help, I would try that first. Then we'll check the comparison function of memo if it does not work, but it should indeed prevent unmodified components from rerendering already as you've done it. – Camille Hg Jan 14 '23 at 20:08
  • Thanks, that's a great suggestion. I just moved the onPressPriority function into the Task component - the flatlist still renders when I press it. Does that isolate the problem to the memoization of the Task component itself? Rather than the memoization of the function. – Lloyd Rajoo Jan 15 '23 at 00:04
  • 1
    @CamilleHg are you sure keyExtractor won't help here? I think the reference of the list changing won't defeat it - FlatList uses those keys to choose what and what not to re-render. It's a best practice anyway and I recommend OP try it. – Abe Jan 15 '23 at 01:19
  • 1
    Yes keyExtractor is recommended anyway but I don't think this will solve the problem, we can still try. Indeed the memoization of the function is not necessary once moved into the Task component. To debug why the not updated components are rerendered, I suggest you pass a custom [equality check function](https://reactjs.org/docs/react-api.html#reactmemo) (areEqual in the exemple) to `memo` and log the props received by each component (prevProps ans nextProps). The components should not render if props are the same. If you cannot achieve it with this function, then consider my first response. – Camille Hg Jan 15 '23 at 09:41
  • 1
    You're right, it does isolate the problem to the memoization of the component itselft. – Camille Hg Jan 15 '23 at 09:42
  • Unfortunately I've added keyExtractor={(item) => item.id} and it does not seem to help. I believe since I already had an id property, it wasn't required for my Flatlist. @CamilleHg, thanks for suggesting to call the firebase onValue method in each task component - my knowledge of caching policy is practically zero so I may need to defer on that - and I really am very interested in finding out why my memoization isn't working. – Lloyd Rajoo Jan 15 '23 at 23:49
  • I added a custom equality function into the memoized task component - unfortunately it still keeps rendering! Haha... – Lloyd Rajoo Jan 16 '23 at 16:44
  • How do you check the rerenders, with a console.log in Task? This is really strange. – Camille Hg Jan 16 '23 at 18:56
  • I used a console.log(item) in Task as well as the Flamegraph. I added the console.log that I'm using here as well. I also further simplified the code and I added the change you mentioned to push the function into the component - so the problem should be even clearer now. – Lloyd Rajoo Jan 16 '23 at 23:56
  • 1
    I took a look a the new code, and I was able to reproduce it. The problem comes from your comparison function, it must return true if the result IS the same. On my side it worked with ` if ( prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) { return true; } return false;` I think that the default memo fails because it is a shallow comparison based on the reference of the `item` objects, which change at each update because you push new `item` objects in the state. – Camille Hg Jan 17 '23 at 10:03
  • Would you want to add this as a separate answer, or should I mark this answer with the comments chain as the answer? – Lloyd Rajoo Jan 17 '23 at 12:06
  • 1
    I'll update my first answer so it's clearer, I'm glad we finally found it ;) – Camille Hg Jan 17 '23 at 13:23
  • 1
    thanks so much! really appreciate the time you've taken on this. – Lloyd Rajoo Jan 17 '23 at 14:25
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/251216/discussion-between-lloyd-rajoo-and-camille-hg). – Lloyd Rajoo Jan 17 '23 at 16:02