skip to Main Content

i am trying to understand react memo and i created a simple interface in my react native app. the app consists of two elements:

MainApp.tsx -> controls the list of the user

User.tsx -> displays user object

my plan is to have all user information displayed on first render and each user should have some sort of "update" button which would case it to re-render. The user would be passed to the list item component along with a description. If the description changes, the whole list should be re-rendered. The current implementation looks like this:

mainapp:

// MainApp component
import React, { useState } from 'react';
import { StyleSheet, Button, SafeAreaView, FlatList } from 'react-native';
import User from './User';

export interface IUser {
  name: string;
  id: number;
  age: number;
}

const initialUsers: IUser[] = [
  { id: 1, name: 'Ivan', age: 20 },
  { id: 2, name: 'Elena', age: 25 },
  { id: 3, name: 'John', age: 30 },
];

export const MainApp = () => {
  const [users, setUsers] = useState<IUser[]>(initialUsers);
  const [description, setDescription] = useState<string>(
    'A passionate developer',
  );

  const updateUserAge = (userId: number) => {
    setUsers(
      users.map(user =>
        user.id === userId
          ? { ...user, age: Math.floor(Math.random() * (100 - 20 + 1)) + 20 }
          : user,
      ),
    );
  };

  const updateDescription = () => {
    setDescription(
      (Math.floor(Math.random() * (100 - 20 + 1)) + 20).toString(),
    );
  };

  return (
    <SafeAreaView style={styles.container}>
      <FlatList
        data={users}
        keyExtractor={item => item.id.toString()}
        renderItem={({ item }) => (
          <User
            user={item}
            description={description}
            onUpdateAge={updateUserAge}
          />
        )}
      />
      <Button onPress={updateDescription} title="Update Description" />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
});

export default MainApp;

user.tsx

import React, { memo } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import { IUser } from './MainApp';

interface UserProps {
  user: IUser;
  description: string;
  onUpdateAge: (userId: number) => void;
}

export const User = memo(
  ({ user, description, onUpdateAge }: UserProps) => {
    console.log('Rendering User', user.name);

    const handleUpdateAge = () => {
      onUpdateAge(user.id);
    };

    return (
      <View style={styles.container}>
        <Text>Name: {user.name}</Text>
        <Text>Age: {user.age}</Text>
        <Text>Description: {description}</Text>
        <Button onPress={handleUpdateAge} title="Update Age" />
      </View>
    );
  },
  (prevProps, nextProps) => {
    return (
      prevProps.user.age === nextProps.user.age &&
      prevProps.description === nextProps.description
    );
  },
);

const styles = StyleSheet.create({
  container: {
    margin: 10,
    padding: 10,
    backgroundColor: '#eee',
  },
});

export default User;

since the object reference stays the same, i specify what props to compare. When i click on the first element i get:

LOG Rendering User Ivan

which is correct and the whole list was not re-rendered, only one item is updated.

however, if i click on another list item after that i get this:

 LOG  Rendering User Ivan
 LOG  Rendering User Elena

For some reason two list items were updated and it keeps going if i click on another users. Can you help me understand why the list items are re-rendered?

passing user‘s fields separately still produces the same problem:
https://snack.expo.dev/@denistepp/hot-orange-nachos

2

Answers


  1. Chosen as BEST ANSWER

    the problem was in updateUserAge actually. The dependency on users always created a new function causing a re-render. here is the fixed solution:

      const updateUserAge = useCallback(
        (userId: number) => {
          setUsers(currentUsers =>
            currentUsers.map(user =>
              user.id === userId
                ? { ...user, age: Math.floor(Math.random() * (100 - 20 + 1)) + 20 }
                : user,
            ),
          );
        },
        [],
      );
    

    considering the description prop:

    1. if you want all list items to be re-rendered if description changes you can just use memo wrap and nothing else. in this case the main app render function would look like this:
      const renderItem = useCallback(
        ({ item }: { item: IUser }) => {
          return (
            <User
              user={item}
              description={description}
              onUpdateAge={updateUserAge}
            />
          );
        },
        [description, updateUserAge],
      );
    
    1. if you don't want the items to be re-rendered when description changes, but still need to pass the description for some reason you can modify memo's second parameter to do a custom comparison of the props:
    export const User = memo(
      ({ user, description, onUpdateAge }: UserProps) => {
        console.log('Rendering User', user.name);
    
        const handleUpdateAge = () => {
          onUpdateAge(user.id);
        };
    
        return (
          <View style={styles.container}>
            <Text>Name: {user.name}</Text>
            <Text>Age: {user.age}</Text>
            <Text>Description: {description}</Text>
            <Button onPress={handleUpdateAge} title="Update Age" />
          </View>
        );
      },
      (prevProps, nextProps) => {
        return prevProps.user.age === nextProps.user.age;
      },
    );
    

    in both cases the renderItem could be simplified and look like this:

      const renderItem = useCallback(
        ({ item }: { item: IUser }) => {
          return (
            <User
              user={item}
              description={description}
              onUpdateAge={updateUserAge}
            />
          );
        },
        [description, updateUserAge],
      );
    

  2. Remember

    objects and Arrays inside the memo/useMemo/useCallback dependencies always will be different ’cause the check is by reference not by value. So you have to use whenever possible primitive values –bolean, number, string–.

    Performance ⬆️

    Second, try to move your renderItem component to a memorized function out of your JSX return block:

    const renderItem = useCallback(
        ({ item }) => (
          <User user={item} description={description} onUpdateAge={updateUserAge} />
        ),
        [description, updateUserAge]
      );
    

    This will let you know that every time description or updateUserAge changes your component will be re-rendered. If you want to avoid that, move the description into the item itself.

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