Redux это бойлерплейт, а Mobx нет! Но есть нюанс
- воскресенье, 21 апреля 2024 г. в 00:00:10
На прошлой неделе впервые поучаствовал в конференции по 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 точно нет. Для себя я все таки сделаю вывод, что оба инструмента не заставляют разработчика писать "тону" шаблонного кода.