Skip to content

Commit

Permalink
feat(recipes): new Zustand recipe, and separate recipe for removing M…
Browse files Browse the repository at this point in the history
…obX-State-Tree (#121 by @Jpoliachik)

* created RemoveMobxStateTree recipe

* updated Redux recipe to reference new RemoveMobx post

* add step to remove useStores()

* Zustand recipe

* update dates

* MobX not Mobx

* update Remove MST recipe

* redux post MobX not Mobx

* Zustand post fixes
  • Loading branch information
Jpoliachik authored Feb 13, 2024
1 parent 395d310 commit 6d7c612
Show file tree
Hide file tree
Showing 3 changed files with 879 additions and 101 deletions.
119 changes: 18 additions & 101 deletions docs/recipes/Redux.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Redux
description: How to migrate a Mobx-State-Tree project to Redux
description: How to migrate a MobX-State-Tree project to Redux
tags:
- Redux
- MobX
Expand All @@ -12,94 +12,17 @@ publish_date: 2024-01-16

# Redux

This guide will show you how to migrate a Mobx-State-Tree project (Ignite's default) to Redux, using a newly created Ignite project as our example:
This guide will show you how to migrate a MobX-State-Tree project (Ignite's default) to Redux, using a newly created Ignite project as our example:

```terminal
npx ignite-cli new ReduxApp --yes --removeDemo
```

If you are migrating an existing project these steps still apply, but you may need to migrate your existing state tree and other additional functionality.

## Remove Mobx-State-Tree
## Remove MobX-State-Tree

- Remove all Mobx-related dependencies from `package.json`, then run `yarn` or `npm i`

```diff
--"mobx": "6.10.2",
--"mobx-react-lite": "4.0.5",
--"mobx-state-tree": "5.3.0",

--"reactotron-mst": "3.1.5",
```

- Ignite created default boilerplate Mobx-State-Tree files in the `models/` directory. Remove this entire directory and all files within it, these are not needed for Redux.

- In `devtools/ReactotronConfig.ts` remove the `reactotron-mst` plugin. We can come back to [add a Redux plugin](#reactotron-support) later.

```diff
--import { mst } from "reactotron-mst"

...

const reactotron = Reactotron.configure({
name: require("../../package.json").name,
onConnect: () => {
/** since this file gets hot reloaded, let's clear the past logs every time we connect */
Reactotron.clear()
},
--}).use(
-- mst({
-- /** ignore some chatty `mobx-state-tree` actions */
-- filter: (event) => /postProcessSnapshot|@APPLY_SNAPSHOT/.test(event.name) === false,
-- }),
--)
++})
```

- Remove all `observer()` components and reformat as normal React components. Do a project-wide search for `observer(` and replace each component instance with the following pattern:

```diff
--import { observer } from "mobx-react-lite"

--export const WelcomeScreen: FC<WelcomeScreenProps> = observer(function WelcomeScreen(props) {
++export const WelcomeScreen: FC<WelcomeScreenProps> = (props) => {
...
--})
++}
```

- (optional) Don't forget to update your [Ignite Generator Templates](https://docs.infinite.red/ignite-cli/concept/Generator-Templates/)!
- Follow the same pattern to replace `observer()`. This will allow you to quickly generate screens and components via `npx ignite-cli generate screen NewScreen` and `npx ignite-cli generate component NewComponent` and use your updated syntax. (You can customize these however you like!)
- Update `ignite/templates/component/NAME.tsx.ejs` and `ignite/templates/screen/NAMEScreen.tsx.ejs`

```diff
--import { observer } from "mobx-react-lite"

--export const <%= props.pascalCaseName %> = observer(function <%= props.pascalCaseName %>(props: <%= props.pascalCaseName %>Props) {
++export const <%= props.pascalCaseName %> = (props: <%= props.pascalCaseName %>Props) => {
...
--})
++}
```

- Remove old Mobx-State-Tree store initialization / hydration code in `app.tsx`.
- Call `hideSplashScreen` in a `useEffect` so the app loads for now. We'll replace this code when we add [persistence](#persistence) below.

```diff
--import { useInitialRootStore } from "./models"

--const { rehydrated } = useInitialRootStore(() => {
--setTimeout(hideSplashScreen, 500)
--})
++useEffect(() => {
++ setTimeout(hideSplashScreen, 500)
++}, [])

--if (!rehydrated || !isNavigationStateRestored || !areFontsLoaded) return null
++if (!isNavigationStateRestored || !areFontsLoaded) return null
```

You should be able to build and run your app! It won't have any data...but it's a good idea to check that it successfully runs before we move on.
First, follow our recipe to [Remove MobX-State-Tree](./RemoveMobxStateTree.md) from your project. This will give you a blank slate to setup Redux.

## Add Redux

Expand All @@ -114,11 +37,12 @@ yarn add react-redux

#### Create Store

- In a new file `app/store.ts`, create your Redux store.
- In a new file `app/store/store.ts`, create your Redux store.
- Create an initial store. We're using [Redux Toolkit's `configureStore`](https://redux-toolkit.js.org/usage/usage-guide#simplifying-store-setup-with-configurestore) here for simplicity.
- Export Typescript helpers for the rest of your app to stay type safe
- Export Typescript helpers for the rest of your app to stay type safe.
- We'll use `app/store` directory for all our Redux reducers and store, but feel free to use any directory structure you like. Another popular option is to use [feature folders](https://redux.js.org/faq/code-structure).

`store.ts`
**`app/store/store.ts`**

```typescript
import { configureStore } from "@reduxjs/toolkit";
Expand All @@ -145,10 +69,10 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
#### Add State

- Add your state reducers or [slices](https://redux-toolkit.js.org/usage/usage-guide#creating-slices-of-state). We'll create a simple `counter` slice for this example.
- If you have an existing state tree with Mobx-State-Tree, you'll need to convert your tree into a series of Redux reducers.
- Note: Redux does not define or validate your models like Mobx-State-Tree does. It is up to you to ensure the correct data is being set in your reducers.
- If you have an existing state tree with MobX-State-Tree, you'll need to convert your tree into a series of Redux reducers.
- Note: Redux does not define or validate your models like MobX-State-Tree does. It is up to you to ensure the correct data is being set in your reducers.

`counterSlice.ts`
**`app/store/counterSlice.ts`**

```typescript
import { createSlice } from "@reduxjs/toolkit";
Expand Down Expand Up @@ -185,6 +109,8 @@ export default counterSlice.reducer;

In `app.tsx`, wrap your `AppNavigator` with the react-redux Provider component

**`app/app.tsx`**

```jsx
import { Provider } from "react-redux";
import { store } from "./store/store";
Expand All @@ -206,7 +132,7 @@ You can now use selectors to grab data and `dispatch()` to execute actions withi

- Remember to use our exported `useAppSelector` and `useAppDispatch` helpers for type safety

`WelcomeScreen.tsx`
**`app/screens/WelcomeScreen.tsx`**

```typescript
import React, { FC } from "react";
Expand Down Expand Up @@ -243,7 +169,7 @@ You're now using Redux!

## Persistence

Ignite ships with built-in persistence support for Mobx-State-Tree. We can add similar support for Redux by:
Ignite ships with built-in persistence support for MobX-State-Tree. We can add similar support for Redux by:

1. Install [`redux-persist`](https://github.com/rt2zz/redux-persist)

Expand All @@ -253,22 +179,13 @@ yarn add redux-persist

2. Modify `store.ts` to include `redux-persist`

`store.ts`
**`app/store/store.ts`**

```typescript
import { combineReducers, configureStore } from "@reduxjs/toolkit";
import counterReducer from "./counterSlice";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import {
persistStore,
persistReducer,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from "redux-persist";
import { persistStore, persistReducer, FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER } from "redux-persist";
import AsyncStorage from "@react-native-async-storage/async-storage";
const persistConfig = {
Expand Down Expand Up @@ -307,7 +224,7 @@ export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

3. Add a `PersistGate` to `app.tsx` and replace any existing `hideSplashScreen` calls with the `onBeforeLift` callback

`app.tsx`
**`app/app.tsx`**

```typescript
...
Expand Down
159 changes: 159 additions & 0 deletions docs/recipes/RemoveMobxStateTree.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
---
title: Remove MobX-State-Tree
description: How to remove MobX-State-Tree from an Ignite project
tags:
- MobX
- State Management
last_update:
author: Justin Poliachik
publish_date: 2024-02-05
---

# Remove Mobx-State-Tree

By default, Ignite uses [MobX-State-Tree](https://mobx-state-tree.js.org/) as the default state management solution. While we love [MobX-State-Tree at Infinite Red](https://docs.infinite.red/ignite-cli/concept/MobX-State-Tree/), we understand the landscape is rich with great alternatives that you may want to use instead.

This guide will show you how to remove Mobx-State-Tree from an Ignite-generated project and get to a "blank slate" with no state management at all.

## Steps

1. Let's start by removing all MobX-related dependencies

```bash
yarn remove mobx mobx-react-lite mobx-state-tree reactotron-mst
```

2. Ignite places all MobX-State-Tree models in the `models/`. Remove this entire directory and all files within it, these are not needed anymore.

```terminal
rm -rf ./app/models
```

:::note
If you are migrating a project with several existing models, you may want to keep a copy of these around for reference as you migrate to your new system.
:::

3. Remove the `reactotron-mst` plugin from Reactotron's config

**devtools/ReactotronConfig.ts**

```diff
--import { mst } from "reactotron-mst"

...

const reactotron = Reactotron.configure({
name: require("../../package.json").name,
onConnect: () => {
/** since this file gets hot reloaded, let's clear the past logs every time we connect */
Reactotron.clear()
},
--}).use(
-- mst({
-- /** ignore some chatty `mobx-state-tree` actions */
-- filter: (event) => /postProcessSnapshot|@APPLY_SNAPSHOT/.test(event.name) === false,
-- }),
--)
++})
```

4. Remove `observer()` wrapped components and reformat as functional React components

- Do a project-wide search for `observer(` and replace each component instance with the following pattern:

**app/screens/WelcomeScreen.tsx**

```diff
--import { observer } from "mobx-react-lite"

--export const WelcomeScreen: FC<WelcomeScreenProps> = observer(function WelcomeScreen(props) {
++export const WelcomeScreen: FC<WelcomeScreenProps> = (props) => {
...
--})
++}
```

5. Remove `useStores()` from components

- Do a project-wide search for `useStores` and remove each instance.
- If you're converting to a different state management solution, you'll need to swap the data we get from `useStores` to your new solution. Or you can swap in temporary hardcoded values to prevent crashes while you migrate. (just don't forget about it!)

```diff
--import { useStores } from "../models"

const AppStack = () => {
-- const { authenticationStore: { isAuthenticated } } = useStores()
++ const isAuthenticated = false // TODO: TEMPORARY VALUE - replace with alternative state management solution
```

6. Update the [Ignite Generator Templates](https://docs.infinite.red/ignite-cli/concept/Generator-Templates/)!

- Follow the same pattern to replace `observer()`. This will allow you to quickly generate screens and components via `npx ignite-cli generate screen NewScreen` and `npx ignite-cli generate component NewComponent` and use your updated syntax.
- I also recommend customizing these however else you prefer!

**ignite/templates/component/NAME.tsx.ejs**
**ignite/templates/screen/NAMEScreen.tsx.ejs**

```diff
--import { observer } from "mobx-react-lite"

--export const <%= props.pascalCaseName %> = observer(function <%= props.pascalCaseName %>(props: <%= props.pascalCaseName %>Props) {
++export const <%= props.pascalCaseName %> = (props: <%= props.pascalCaseName %>Props) => {
...
--})
++}
```

7. Remove old MobX-State-Tree store initialization & hydration code in `app.tsx`.

- We still need to call `hideSplashScreen` in a `useEffect` so the app loads without needing to hydrate a store first.

**app/app.tsx**

```diff
--import { useInitialRootStore } from "./models"

--const { rehydrated } = useInitialRootStore(() => {
--setTimeout(hideSplashScreen, 500)
--})
++React.useEffect(() => {
++ setTimeout(hideSplashScreen, 500)
++}, [])

--if (!rehydrated || !isNavigationStateRestored || !areFontsLoaded) return null
++if (!isNavigationStateRestored || !areFontsLoaded) return null
```

8. Remove any remaining `/models` imports

Your app might have a few remaining references to replace. In the Ignite Demo App, we need to replace the `EpisodeSnapshotIn` type which was previously derived from the MST model. Instead, we'll use `EpisodeItem` from our API types.

**app/services/api/api.ts**

```diff
--import type { ApiConfig, ApiFeedResponse } from "./api.types"
--import type { EpisodeSnapshotIn } from "../../models/Episode"
++import type { ApiConfig, ApiFeedResponse, EpisodeItem } from "./api.types"


--async getEpisodes(): Promise<{ kind: "ok"; episodes: EpisodeSnapshotIn[] } | GeneralApiProblem> {
++async getEpisodes(): Promise<{ kind: "ok"; episodes: EpisodeItem[] } | GeneralApiProblem> {
// make the api call

--// This is where we transform the data into the shape we expect for our MST model.
--const episodes: EpisodeSnapshotIn[] =
-- rawData?.items.map((raw) => ({
-- ...raw,
-- })) ?? []
++const episodes = rawData?.items ?? []
```

## Conclusion

You should be able to build and run your app! It won't have any data...but you now have a "blank slate" to setup your state management solution of choice.

For next steps, we have recipes for migrating to

- [Redux](./Redux.md)
- [Zustand](./Zustand.md)
- Or you can roll your own!
Loading

0 comments on commit 6d7c612

Please sign in to comment.