Skip to content

Commit 5576dcc

Browse files
authored
feat(recipes): redux recipe (#117 by @Jpoliachik)
* add redux recipe * additional clarity and refined examples * add full filepath for store.ts * add info about updating ignite generator templates
1 parent 22d21b5 commit 5576dcc

File tree

1 file changed

+363
-0
lines changed

1 file changed

+363
-0
lines changed

docs/recipes/Redux.md

+363
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
---
2+
title: Redux
3+
description: How to migrate a Mobx-State-Tree project to Redux
4+
tags:
5+
- Redux
6+
- MobX
7+
- State Management
8+
last_update:
9+
author: Justin Poliachik
10+
publish_date: 2024-01-16
11+
---
12+
13+
# Redux
14+
15+
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:
16+
17+
```terminal
18+
npx ignite-cli new ReduxApp --yes --removeDemo
19+
```
20+
21+
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.
22+
23+
## Remove Mobx-State-Tree
24+
25+
- Remove all Mobx-related dependencies from `package.json`, then run `yarn` or `npm i`
26+
27+
```diff
28+
--"mobx": "6.10.2",
29+
--"mobx-react-lite": "4.0.5",
30+
--"mobx-state-tree": "5.3.0",
31+
32+
--"reactotron-mst": "3.1.5",
33+
```
34+
35+
- 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.
36+
37+
- In `devtools/ReactotronConfig.ts` remove the `reactotron-mst` plugin. We can come back to [add a Redux plugin](#reactotron-support) later.
38+
39+
```diff
40+
--import { mst } from "reactotron-mst"
41+
42+
...
43+
44+
const reactotron = Reactotron.configure({
45+
name: require("../../package.json").name,
46+
onConnect: () => {
47+
/** since this file gets hot reloaded, let's clear the past logs every time we connect */
48+
Reactotron.clear()
49+
},
50+
--}).use(
51+
-- mst({
52+
-- /** ignore some chatty `mobx-state-tree` actions */
53+
-- filter: (event) => /postProcessSnapshot|@APPLY_SNAPSHOT/.test(event.name) === false,
54+
-- }),
55+
--)
56+
++})
57+
```
58+
59+
- 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:
60+
61+
```diff
62+
--import { observer } from "mobx-react-lite"
63+
64+
--export const WelcomeScreen: FC<WelcomeScreenProps> = observer(function WelcomeScreen(props) {
65+
++export const WelcomeScreen: FC<WelcomeScreenProps> = (props) => {
66+
...
67+
--})
68+
++}
69+
```
70+
71+
- (optional) Don't forget to update your [Ignite Generator Templates](https://docs.infinite.red/ignite-cli/concept/Generator-Templates/)!
72+
- 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!)
73+
- Update `ignite/templates/component/NAME.tsx.ejs` and `ignite/templates/screen/NAMEScreen.tsx.ejs`
74+
75+
```diff
76+
--import { observer } from "mobx-react-lite"
77+
78+
--export const <%= props.pascalCaseName %> = observer(function <%= props.pascalCaseName %>(props: <%= props.pascalCaseName %>Props) {
79+
++export const <%= props.pascalCaseName %> = (props: <%= props.pascalCaseName %>Props) => {
80+
...
81+
--})
82+
++}
83+
```
84+
85+
- Remove old Mobx-State-Tree store initialization / hydration code in `app.tsx`.
86+
- Call `hideSplashScreen` in a `useEffect` so the app loads for now. We'll replace this code when we add [persistence](#persistence) below.
87+
88+
```diff
89+
--import { useInitialRootStore } from "./models"
90+
91+
--const { rehydrated } = useInitialRootStore(() => {
92+
--setTimeout(hideSplashScreen, 500)
93+
--})
94+
++useEffect(() => {
95+
++ setTimeout(hideSplashScreen, 500)
96+
++}, [])
97+
98+
--if (!rehydrated || !isNavigationStateRestored || !areFontsLoaded) return null
99+
++if (!isNavigationStateRestored || !areFontsLoaded) return null
100+
```
101+
102+
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.
103+
104+
## Add Redux
105+
106+
#### Install dependencies
107+
108+
[redux-tooklit is the current recommended approach](https://redux.js.org/introduction/getting-started#redux-toolkit), and you'll also need `react-redux` bindings for your React Native app.
109+
110+
```bash
111+
yarn add @reduxjs/toolkit
112+
yarn add react-redux
113+
```
114+
115+
#### Create Store
116+
117+
- In a new file `app/store.ts`, create your Redux store.
118+
- 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.
119+
- Export Typescript helpers for the rest of your app to stay type safe
120+
121+
`store.ts`
122+
123+
```typescript
124+
import { configureStore } from "@reduxjs/toolkit";
125+
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
126+
import counterReducer from "./counterSlice";
127+
128+
export const store = configureStore({
129+
reducer: {
130+
counter: counterReducer,
131+
// add other state here
132+
},
133+
});
134+
135+
// Infer the `RootState` and `AppDispatch` types from the store itself
136+
export type RootState = ReturnType<typeof store.getState>;
137+
export type AppDispatch = typeof store.dispatch;
138+
139+
// Use throughout app instead of plain `useDispatch` and `useSelector` for type safety
140+
type DispatchFunc = () => AppDispatch;
141+
export const useAppDispatch: DispatchFunc = useDispatch;
142+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
143+
```
144+
145+
#### Add State
146+
147+
- 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.
148+
- If you have an existing state tree with Mobx-State-Tree, you'll need to convert your tree into a series of Redux reducers.
149+
- 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.
150+
151+
`counterSlice.ts`
152+
153+
```typescript
154+
import { createSlice } from "@reduxjs/toolkit";
155+
156+
// Define a type for the slice state
157+
interface CounterState {
158+
value: number;
159+
}
160+
161+
// Define the initial state using that type
162+
const initialState: CounterState = {
163+
value: 0,
164+
};
165+
166+
export const counterSlice = createSlice({
167+
name: "counter",
168+
// `createSlice` will infer the state type from the `initialState` argument
169+
initialState,
170+
reducers: {
171+
increment: (state) => {
172+
state.value += 1;
173+
},
174+
decrement: (state) => {
175+
state.value -= 1;
176+
},
177+
},
178+
});
179+
180+
export const { increment, decrement } = counterSlice.actions;
181+
export default counterSlice.reducer;
182+
```
183+
184+
#### Add Redux Provider
185+
186+
In `app.tsx`, wrap your `AppNavigator` with the react-redux Provider component
187+
188+
```jsx
189+
import { Provider } from "react-redux";
190+
import { store } from "./store/store";
191+
192+
...
193+
194+
<Provider store={store}>
195+
<AppNavigator
196+
linking={linking}
197+
initialState={initialNavigationState}
198+
onStateChange={onNavigationStateChange}
199+
/>
200+
</Provider>
201+
```
202+
203+
#### Hook up Components
204+
205+
You can now use selectors to grab data and `dispatch()` to execute actions within your components. Here's an example:
206+
207+
- Remember to use our exported `useAppSelector` and `useAppDispatch` helpers for type safety
208+
209+
`WelcomeScreen.tsx`
210+
211+
```typescript
212+
import React, { FC } from "react";
213+
import { View, ViewStyle } from "react-native";
214+
import { Button, Text } from "app/components";
215+
import { AppStackScreenProps } from "../navigators";
216+
import { colors } from "../theme";
217+
import { useSafeAreaInsetsStyle } from "../utils/useSafeAreaInsetsStyle";
218+
import { useAppDispatch, useAppSelector } from "app/store/store";
219+
import { decrement, increment } from "app/store/counterSlice";
220+
221+
interface WelcomeScreenProps extends AppStackScreenProps<"Welcome"> {}
222+
223+
export const WelcomeScreen: FC<WelcomeScreenProps> = () => {
224+
const $containerInsets = useSafeAreaInsetsStyle(["top", "bottom"]);
225+
const count = useAppSelector((state) => state.counter.value);
226+
const dispatch = useAppDispatch();
227+
return (
228+
<View style={[$containerInsets, $container]}>
229+
<Button text="Increment" onPress={() => dispatch(increment())} />
230+
<Button text="Decrement" onPress={() => dispatch(decrement())} />
231+
<Text text={`Count: ${count}`} />
232+
</View>
233+
);
234+
};
235+
236+
const $container: ViewStyle = {
237+
flex: 1,
238+
backgroundColor: colors.background,
239+
};
240+
```
241+
242+
You're now using Redux!
243+
244+
## Persistence
245+
246+
Ignite ships with built-in persistence support for Mobx-State-Tree. We can add similar support for Redux by:
247+
248+
1. Install [`redux-persist`](https://github.com/rt2zz/redux-persist)
249+
250+
```
251+
yarn add redux-persist
252+
```
253+
254+
2. Modify `store.ts` to include `redux-persist`
255+
256+
`store.ts`
257+
258+
```typescript
259+
import { combineReducers, configureStore } from "@reduxjs/toolkit";
260+
import counterReducer from "./counterSlice";
261+
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
262+
import {
263+
persistStore,
264+
persistReducer,
265+
FLUSH,
266+
REHYDRATE,
267+
PAUSE,
268+
PERSIST,
269+
PURGE,
270+
REGISTER,
271+
} from "redux-persist";
272+
import AsyncStorage from "@react-native-async-storage/async-storage";
273+
274+
const persistConfig = {
275+
key: "root",
276+
version: 1,
277+
storage: AsyncStorage,
278+
};
279+
280+
const rootReducer = combineReducers({
281+
counter: counterReducer,
282+
});
283+
284+
const persistedReducer = persistReducer(persistConfig, rootReducer);
285+
286+
export const store = configureStore({
287+
reducer: persistedReducer,
288+
middleware: (getDefaultMiddleware) =>
289+
getDefaultMiddleware({
290+
serializableCheck: {
291+
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
292+
},
293+
}),
294+
});
295+
296+
export const persistor = persistStore(store);
297+
298+
// Infer the `RootState` and `AppDispatch` types from the store itself
299+
export type RootState = ReturnType<typeof store.getState>;
300+
export type AppDispatch = typeof store.dispatch;
301+
302+
// Use throughout app instead of plain `useDispatch` and `useSelector` for type safety
303+
type DispatchFunc = () => AppDispatch;
304+
export const useAppDispatch: DispatchFunc = useDispatch;
305+
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
306+
```
307+
308+
3. Add a `PersistGate` to `app.tsx` and replace any existing `hideSplashScreen` calls with the `onBeforeLift` callback
309+
310+
`app.tsx`
311+
312+
```typescript
313+
...
314+
315+
import { persistor, store } from "./store/store"
316+
import { PersistGate } from "redux-persist/integration/react"
317+
318+
...
319+
320+
function App(props: AppProps) {
321+
const { hideSplashScreen } = props
322+
...
323+
const onBeforeLiftPersistGate = () => {
324+
// If your initialization scripts run very fast, it's good to show the splash screen for just a bit longer to prevent flicker.
325+
// Slightly delaying splash screen hiding for better UX; can be customized or removed as needed,
326+
// Note: (vanilla Android) The splash-screen will not appear if you launch your app via the terminal or Android Studio. Kill the app and launch it normally by tapping on the launcher icon. https://stackoverflow.com/a/69831106
327+
// Note: (vanilla iOS) You might notice the splash-screen logo change size. This happens in debug/development mode. Try building the app for release.
328+
setTimeout(hideSplashScreen, 500)
329+
}
330+
...
331+
return (
332+
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
333+
<ErrorBoundary catchErrors={Config.catchErrors}>
334+
<GestureHandlerRootView style={$container}>
335+
<Provider store={store}>
336+
<PersistGate
337+
loading={null}
338+
onBeforeLift={onBeforeLiftPersistGate}
339+
persistor={persistor}
340+
>
341+
<AppNavigator
342+
linking={linking}
343+
initialState={initialNavigationState}
344+
onStateChange={onNavigationStateChange}
345+
/>
346+
</PersistGate>
347+
</Provider>
348+
</GestureHandlerRootView>
349+
</ErrorBoundary>
350+
</SafeAreaProvider>
351+
)
352+
}
353+
354+
export default App
355+
```
356+
357+
Your Redux state should now be persisted using AsyncStorage!
358+
359+
## Reactotron Support
360+
361+
Reactotron has a prebuilt plugin for Redux!
362+
363+
[Follow the instructions to install](https://docs.infinite.red/reactotron/plugins/redux/)

0 commit comments

Comments
 (0)