В продолжение вчерашней темы, про миграцию на mobx
резонно возникает вопрос, а зачем вообще мигрировать на что-то, если есть redux
, context api
, useReducer
и всякие подобные решения на которых строится сейчас React
разработка.
export const AppContext = createContext({});
export const AppContextProvider: FC = ({ children }) => {
const [user, setUser] = useState<IUser>(undefined);
const [profile, setProfile] = useState<IProfile>(undefined);
const setUserId = (id: string) => loadUser(id).then(setUser);
useEffect(() => {
if (user) {
loadProfile(user).then(setProfile);
} else {
setProfile(undefined);
}
}, [user]);
return (
<AppContext.Provider values={{ user, profile, setUserId }}>
{children}
</AppContext.Provider>);
}
const Component: FC = () => {
const { profile } = useContext(AppContext);
return <div>{profile.content}</div>
}
Представим виртуальный пример, который достаточно просто встретить во многих проектах. Что в нем не так?
Проведем мысленный эксперимент. У нас есть компонент, который отображает profile
. Сколько раз перерендерится наш компонент, когда мы вызовем setUserId
. Первый ре-рендер будет на setUser
, потом у нас вызовется useEffect
. Его бы я вообще примерно никогда не использовал для работы со стейтом. Далее setProfile
и это уже второй ререндер. Такое же поведение будет в каждом компоненте, использующим этот контекст.
И неоптимальность - это полбеды, но если, например, мы хотим небольшой тест, чтобы проверить, как это все работает… я даже думать об этом не хочу и перехожу в лагерь тех кто не пишет тесты и рассуждают о них только на собеседовании.
Давате перепишем все на mobx и получаем один ререндер. Потому что мы подписываемся только на изменение profile
.
class AppModel {
user: IUser | undefined = undefined;
profile: IProfile | undefined = undefined;
constructor(
private loadUser: typeof loaduser,
private loadProfile: typeof loadProfile
) {
makeObservable(this, {
user: observable.ref,
profile: observable.ref,
});
}
async setUserId(id: string) {
const user = await this.loadUser(id);
runInAction(() => this.user = user);
const profile = await this.loadProfile(user);
runInAction(() => this.profile = profile);
}
}
// и перепишем компонент
const Component: FC = observer<{ model: AppModel }>(({ model }) => {
return <div>{model.profile.content}</div>
})
А теперь перейдем в лигу тех кто уверен в своем коде (хотя бы бизнес логики) и напишем маленький тест.
test('AppModel', () => {
const user: IUser = { name: 'test' };
const model = new AppModel(
() => Promise.resolve(user),
(user) => Promise.resolve<IProfile({ user, content: 'content'}),
);
model.setUserId('1');
await when(() => model.user !== undefined);
expect(model.user).toBe(user);
expect(model.profile).not.toBeUndefined();
await when(() => model.profile !== undefined);
expect(model.profile).toEqual({ user, content: 'content' });
});