reactjsmobxreact-usememo

Problem with Mobx (TS) using with React Functional Components


Here is the problem.

I have simple todo store:

import { makeAutoObservable } from "mobx";
import { Todo } from "./../types";

class Todos {
todoList: Todo[] = [
    { id: 0, description: "Погулять с собакой", completed: false },
    { id: 1, description: "Полить цветы", completed: false },
    { id: 2, description: "Покормить кота", completed: false },
    { id: 3, description: "Помыть посуду", completed: true },
];

// Input: Add Task
taskInput: string = "";

// Filter: query
query: string = "";

// Filter: showOnlyCompletedTasks
showOnlyCompleted: boolean = false;

constructor() {
    makeAutoObservable(this);
}

setShowOnlyCompletedState(value: boolean) {
    this.showOnlyCompleted = value;
}

changeCompletionState(id: number) {
    const task = this.todoList.find((todo) => todo.id === id);
    if (task) task.completed = !task.completed;
}

addTask(text: string) {
    if (text !== "") {
        const newTodo: Todo = {
            id: Number(new Date()),
            description: text,
            completed: false,
        };
        this.todoList.push(newTodo);
    }
}

taskChangeInput(value: string) {
    this.taskInput = value;
}

queryChangeInput(value: string) {
    this.query = value;
}
}

export default new Todos();

In the app I have some tasks, which I can make completed or not-completed (by clicking on it) and also I do have some filters to filter my todo_list. Here is the code:

import { Todo } from "../types";
import { useMemo } from "react";

function useFilterByQuery (list: Todo[], query: string):Todo[] {
    const filteredList = useMemo(()=>{
        if (!query) return list
        return list.filter(todo => todo.description.toLowerCase().includes(query.toLowerCase()))
    }, [list, query])
    return filteredList
}

export function useFilterByAllFilters (list:Todo[], query: string, showOnlyCompleted: boolean):Todo[] {
    const filteredByQuery = useFilterByQuery(list, query)

    const filteredList = useMemo(()=>{
        if(!showOnlyCompleted) return filteredByQuery
        return filteredByQuery.filter(todo => todo.completed)
    }, [filteredByQuery, showOnlyCompleted])

    return filteredList
}

So the description of the problem is so: when I choose show me only-Completed-tasks (setting showOnlyCompleted to true), I get expected list of tasks which are all 'completed'. But, when I change the state of 'todo' right now, the shown list isn't changing (uncompleted task doesn't filter immediatly), it's changing only after I set showOnlyCompleted to false and back to true.

I assume it's not happening, because I don't 'update' the todoList for MobX, which I provide (using function useFilterByAllFilters) by props in My TodoList component. In my opinion the problem is with the useMemo or with something in Mobx. Please, help me out.


Solution

  • Yes, it's because of useMemo. useFilterByQuery only has [list, query] deps, but when you change some todo object to be (un)completed you don't really change the whole list, only one object inside of it. And that is why this useMemo does not rerun/recompute, so you still have the old list. Same with useFilterByAllFilters.

    What you are doing is not really idiomatic MobX I would say! MobX is designed to work with mutable data and React hooks are designed to work with immutable data, so to make it work with hooks you need to do some workarounds, but it would be easier to just rewrite it like that:

    class Todos {
      // ... your other code
    
      // Add computed getter property to calculate filtered list
      get listByQuery() {
        if (!this.query) return list
        return this.todoList.filter(todo => todo.description.toLowerCase().includes(this.query.toLowerCase()))
      }
    
      // Another one for all filters
      get listByAllFilters() {
        if(!this.showOnlyCompleted) return this.listByQuery
        return this.listByQuery.filter(todo => todo.completed)
      }
    }
    

    And just use this two computed properties in your React components, that's it! No need for hooks. And these properties are cached/optimized, i.e. will only run when some of their observables change.

    More info about computeds