
На прошлой неделе впервые поучаствовал в конференции по 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<typeof PostListSchema> /* Запрос на получение данных */ export const fetchPostList = async (limit: number) => { try { const response = await _api.get<PostListModel[]>(`api/posts`) if (!response.data) { throw new Error("Ошибка") } return response.data.data } catch { throw new Error("Ошибка") } } /* Создание стора */ class PostListStore { posts?: IPromiseBasedObservable<PostListModel[]> 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"; /* Типизация */ 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(fetchPostListAsync.pending, (state) => { state.loading = true; state.error = null; }) .addCase(fetchPostListAsync.fulfilled, (state, action: PayloadAction<PostListState[]>) => { state.loading = false; state.posts = action.payload; }) .addCase(fetchPostListAsync.rejected, (state, action) => { state.loading = false; state.error = action.error.message ?? "Ошибка"; }); }, }); export const { incrementCounter, decrementCounter } = postListSlice.actions; export default postListSlice.reducer; /* Запрос на получение данных */ export const fetchPostListAsync = createAsyncThunk("fetchPostList", async () => { 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 <div>Loading...</div>; case "rejected": return <div>Error: Failed to fetch posts</div>; case "fulfilled": return ( <div> {postListStore.posts?.value.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.description}</p> {/* Другие поля поста */} </div> ))} </div> ); 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 <div>Loading...</div>; if (error) return <div>Error: {error}</div>; if (!posts) return null; return ( <div> {posts.map((post) => ( <div key={post.id}> <h2>{post.title}</h2> <p>{post.description}</p> {/* Другие поля поста */} </div> ))} </div> ); }; export default PostListRedux;
Опять же результат примерно одинаковый в плане количества кода. Однако решение с использованием Redux-Toolkit смотрится проще.
На мой взгляд само сравнение не Toolkit версии с Mobx, крайне странно. Я думаю, это сравнение имело актуальность в 2020 году может быть, но в 2024 точно нет. Для себя я все таки сделаю вывод, что оба инструмента не заставляют разработчика писать "тону" шаблонного кода.
