Home > Software engineering >  Getting "Can't perform a React state update on an unmounted component" only the first
Getting "Can't perform a React state update on an unmounted component" only the first

Time:06-30

I am creating a ToDo app. This app has two screens: Todos and Done. I'm using BottomTabNavigator to switch between these screens. These two screens has list of todos. The todos component shows the undone todos and the Done component shows the done todos. There's a checkbox on the left and Trash icon on the right of every single todo. When a todo from Todos page is checked then it moves to the Done page. The issue is: after switching to the Done screen from Todos for the first time then after unchecking the todo there gives this warning:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. in SingleTodo (at Done.tsx:74) After this, the app is running perfectly. As I'm not sure which component is causing this error that'w why I'm sharing the minimal version of the code.

I have set up Bottom Tab navigator component like this:

import stuff..
...

const HomeTab = () => {
  return (
    <Tab.Navigator
      screenOptions={({route}) => ({
        headerShown: false,
        tabBarIcon: ({focused, color, size}) => {
          let iconName = '';
          size = focused ? 25 : 20;
          if (route.name === 'To-Do') {
            iconName = 'clipboard-list';
          } else if (route.name === 'Done') {
            iconName = 'clipboard-check';
          }

          return <FontAwesome5Icon name={iconName} size={size} color={color} />;
        },
        tabBarActiveTintColor: '#0080ff',
        tabBarInactiveTintColor: '#777777',
        tabBarLabelStyle: {fontSize: 15, fontWeight: 'bold'},
      })}>
      <Tab.Screen name="To-Do" component={Todos} />
      <Tab.Screen name="Done" component={Done} />
    </Tab.Navigator>
  );
};

export default HomeTab;

As you can see, there are 2 components here. One is Todos. The code for this component is as follows:

import stuff...
...

const Todos = ({navigation}) => {
  const dispatch = useAppDispatch();
  const {todos}: {todos: TodoInterface[]} = useAppSelector(
    state => state.todoReducer,
  );

  useEffect(() => {
    loadTodos();
  }, []);

  const loadTodos = () => {
    AsyncStorage.getItem('todos').then(todos => {
      const parsedTodos: TodoInterface[] = JSON.parse(todos || '{}');
      dispatch(setAllTodo(parsedTodos));
    });
  };

  return (
    <HideKeyboard>
      <View style={styles.body}>
        <FlatList
          data={todos.filter(todo => todo.done !== true)}
          renderItem={({item, index}) => {
            const firstChild = index == 0 ? {marginTop: 5} : {};
            return (
              <TouchableOpacity
                style={[styles.todoWrp, firstChild]}
                onPress={() => todoPressHandler(item.todoId)}>
                <SingleTodo  // ***The code for this one is given below***
                  title={item.title}
                  subtitle={item.subTitle}
                  done={item?.done}
                  todoId={item.todoId}
                />
              </TouchableOpacity>
            );
          }}
          keyExtractor={(item, i) => item.todoId}
        />
        <TouchableOpacity style={styles.addBtn} onPress={addBtnHandler}>
          <FontAwesome5 name="plus" color="#fff" size={25} />
        </TouchableOpacity>
      </View>
    </HideKeyboard>
  );
}

The code for SingleTodo is as follows:

const SingleTodo = ({title, subtitle, done: doneProp, todoId}: Props) => {
  const [done, setDone] = useState(doneProp);
  const dispatch = useAppDispatch();
  const {todos}: TodosType = useAppSelector(state => state.todoReducer);

  const checkBoxHandler = (val: boolean) => {
    const todoList: TodoInterface[] = [...todos];
    const index = todos.findIndex(todo => todo.todoId === todoId);
    todoList[index].done = val;
    AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
      dispatch(setAllTodo(todoList));
      setDone(val);
    });
  };

  const deleteHandler = () => {
    const todoList: TodoInterface[] = [...todos];
    const index = todos.findIndex(todo => todo.todoId === todoId);
    todoList.splice(index, 1);
    AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
      dispatch(setAllTodo(todoList));
    });
  };

  return (
    <View style={styles.body}>
      <CheckBox
        value={done}
        onValueChange={val => checkBoxHandler(val)}
        style={styles.checkbox}
      />
      <View>
        <Text style={[styles.title, GlobalStyle.IndieFont]}>{title}</Text>
        <Text style={[styles.subtitle, GlobalStyle.IndieFont]}>{subtitle}</Text>
      </View>
      <View style={styles.trashWrp}>
        <TouchableOpacity onPress={deleteHandler}>
          <FontAwesome5Icon
            style={styles.trashIcon}
            name="trash"
            color="#e74c3c"
            size={20}
          />
        </TouchableOpacity>
      </View>
    </View>
  );
};

export default SingleTodo;

The code for Done component is similar to Todos component. The only changes is on the data property of the component

<FlatList
  data={todos.filter(todo => todo.done === true)}
  ...
  other props...
  ...
  />

CodePudding user response:

It's happening every time you use this, it is just shown once to not spam the console.

 const checkBoxHandler = (val: boolean) => {
    const todoList: TodoInterface[] = [...todos];
    const index = todos.findIndex(todo => todo.todoId === todoId);
    todoList[index].done = val;
    AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
      dispatch(setAllTodo(todoList));
      setDone(val);
    });
  };

  const deleteHandler = () => {
    const todoList: TodoInterface[] = [...todos];
    const index = todos.findIndex(todo => todo.todoId === todoId);
    todoList.splice(index, 1);
    AsyncStorage.setItem('todos', JSON.stringify(todoList)).then(() => {
      dispatch(setAllTodo(todoList));
    });
  };

Basically, you call the function, and the todo is unmounted from the state, but the function is not completed yet and you get that warning.

The solution is to lift everything related to the deleteHandler and checkBoxHandler from your children (Todo) to your parent (Todos), and pass it to Todo as props. Since parent is always mounted, deleting the todo will not unmount the parent and therefore, delete function will not be interrupted.

  • Related