Redux это бойлерплейт, а Mobx нет! Но есть нюанс

IT комьюнити

IT комьюнити

На прошлой неделе впервые поучаствовал в конференции по Frontend, где один из докладчиков, расказывал, как удачно его команда переехала с Redux на Mobx. Главным преимуществом он назвал отсутствие бойлерплейта и ускорение разработки в полтора раза.

Я прочитал несколько статей и посмотрел другие доклады, где все как один говорят, что Mobx лучше, чем Redux. Возможно это и так, но почему в сравнение всегда идет Redux, а не Redux-Toolkit, я не понимаю. Попытаемся конструктивно посмотреть действительно ли Mobx настолько хорош как о нем говорят.

Главный аргумент адептов Mobx звучит примерно так

При разработке на Redux приходится писать тону шаблонного кода, чтобы все работало. Нужно написать action-ы и selectors-ы.

Для примера давайте напишем самую простую логику для запроса постов и изменения счетчика и посмотрим сколько строчек кода мы сможем сэкономить.

import { makeAutoObservable } from "mobx";
import { IPromiseBasedObservable, fromPromise } from "mobx-utils";

/* Типизация */
const PostListSchema = z.object({
  id: z.number(),
  title: z.string(),
  description: z.string(),
  tag: z.string(),
  tags: z.array(z.string()),
  image: z.string(),
  progress: z.number(),
  progressTotal: z.number()
})

type PostListModel = z.infer

/* Запрос на получение данных */
export const fetchPostList = async (limit: number) => {
  try {
    const response = await _api.get(`api/posts`)

    if (!response.data) {
      throw new Error("Ошибка")
    }

    return response.data.data
  } catch {
    throw new Error("Ошибка")
  }
}


/* Создание стора */
class PostListStore {
    posts?: IPromiseBasedObservable
    counter: 0

    constructor() {
        makeAutoObservable(this)
    }

    incrementCounter = () => {
      this.counter += 1
    }

    decrementCounter = () => {
      this.counter -= 1
    }

    fetchCoursesData = (limit: number) => {
        this.courses = fromPromise(fetchPostList(limit))
    }
}

export const postListStore = new PostListStore()

Теперь попробуем написать такую же логику на Redux-Toolkit. Но чтобы избежать предвзятости в нашей оценке давайте попросим chatGPT написать код за нас.

import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { AppThunk } from "./store";
import { fetchPostList, PostListModel } from "./api";

/* Типизация */
interface PostListState {
  posts: PostListModel[] | null;
  loading: boolean;
  error: string | null;
  counter: number;
}

const initialState: PostListState = {
  posts: null,
  loading: false,
  error: null,
  counter: 0,
};

/* Создание слайса */
const postListSlice = createSlice({
  name: "postList",
  initialState,
  reducers: {
    incrementCounter(state) {
      state.counter += 1;
    },
    decrementCounter(state) {
      state.counter -= 1;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchPostList.pending, (state) => {
        state.loading = true;
        state.error = null;
      })
      .addCase(fetchPostList.fulfilled, (state, action: PayloadAction) => {
        state.loading = false;
        state.posts = action.payload;
      })
      .addCase(fetchPostList.rejected, (state, action) => {
        state.loading = false;
        state.error = action.error.message ?? "Ошибка";
      });
  },
});

export const { incrementCounter, decrementCounter } = postListSlice.actions;

export default postListSlice.reducer;

/* Запрос на получение данных */
export const fetchPostListAsync = (limit: number): AppThunk => async (dispatch) => {
  try {
    const response = await axios.get("/api/posts")

    if (!response.data) {
      throw new Error("Ошибка")
    }

    return response.data
  } catch {
    throw new Error("Ошибка")
  }
};

Реализация кода очень похожа, единственное, что в mobx это выглядит немного проще. Однако в сумме разница в 10 строчек, не могу назвать это бойлерплейтом. Actions писать тоже не нужно toolkit все делает за нас.

В рамках эксперимента, давайте попросим chatGPT написать компонент PostList с использованием Mobx и Redux-Toolkit.

/* Код с использованием Mobx */

import React, { useEffect } from "react";
import { observer } from "mobx-react-lite";
import { postListStore } from "../stores/postListStore";
import { IPromiseBasedObservableState } from "mobx-utils";

const PostListMobX: React.FC = observer(() => {
  useEffect(() => {
    postListStore.fetchCoursesData(10); // Загружаем посты при монтировании компонента
  }, []);

  const { state } = postListStore.posts ?? {};

  switch (state) {
    case "pending":
      return 
Loading...
; case "rejected": return
Error: Failed to fetch posts
; case "fulfilled": return (
{postListStore.posts?.value.map((post) => (

{post.title}

{post.description}

{/* Другие поля поста */}
))}
); default: return null; } }); export default PostListMobX;

Возможно стоит отметить, что с кодом для Mobx у GPT возникли трудности и правильный результат удалось получить только с четвертой попытки.

/* Код с использованием Redux-toolkit */

import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "../store";
import { fetchPostListAsync } from "../postListSlice";

const PostListRedux: React.FC = () => {
  const dispatch = useDispatch();
  const { posts, loading, error } = useSelector((state: RootState) => state.postList);

  useEffect(() => {
    dispatch(fetchPostListAsync(10)); // Загружаем посты при монтировании компонента
  }, [dispatch]);

  if (loading) return 
Loading...
; if (error) return
Error: {error}
; if (!posts) return null; return (
{posts.map((post) => (

{post.title}

{post.description}

{/* Другие поля поста */}
))}
); }; export default PostListRedux;

Опять же результат примерно одинаковый в плане количества кода. Однако решение с использованием Redux-Toolkit смотрится проще.

На мой взгляд само сравнение не Toolkit версии с Mobx, крайне странно. Я думаю, это сравнение имело актуальность в 2020 году может быть, но в 2024 точно нет. Для себя я все таки сделаю вывод, что оба инструмента не заставляют разработчика писать «тону» шаблонного кода.

© Habrahabr.ru