Skip to content

Commit 18df9bd

Browse files
authored
add documentation for migrating from V1 (#174)
1 parent 7605f9d commit 18df9bd

File tree

1 file changed

+392
-0
lines changed

1 file changed

+392
-0
lines changed
Lines changed: 392 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,392 @@
1+
---
2+
slug: /Migrating
3+
title: Migrating from V1 👴
4+
---
5+
6+
For the V2, our code of conduct is "keep the code simple and concise" 🤓
7+
8+
## Redux
9+
10+
After the redux-toolkit release, and understood that we don't need the **power**, and the **large among of functionalities** that **Redux Saga** provides,
11+
we conclude that, because we want simple and concise code, we will now use redux-toolkit.
12+
13+
🚨️ **We decided to remove Redux Saga from the boilerplate not because this isn't a good library or a good pattern but for less complexity and use an official and light dependency like redux-toolkit.** 🚨️
14+
15+
| Libraries | V1 | V2 | Goal |
16+
| :-------------------- | :----: | :----: | :---------------------------------------------|
17+
| redux ||| State management |
18+
| redux saga ||| Redux middleware |
19+
| redux sauce ||| Simplify redux syntax |
20+
| redux-toolkit ||| New redux library with some function helpers |
21+
| redux-toolkit-wrapper ||| Easier CRUD redux-toolkit function helpers |
22+
23+
24+
### Migration guide
25+
26+
This is not really a migration guide because there is so much breaking changes between the two versions and mostly because of the update of all dependencies.
27+
So, in next sections, there is a structure and code comparison.
28+
29+
### Architecture
30+
31+
First, a quick comparison on the tree files. On V1 the state management logic was divide in `Services`, `Sagas` and `Store`.
32+
In V2 it is divide in `Service` and `Store`. In V2, all directory as an `index.js` file for better imports and a homogenization of the code.
33+
34+
```jsx title="V1"
35+
Services
36+
- UserService.js
37+
Sagas
38+
- UserSaga.js
39+
- StartupSaga.js
40+
- index.js
41+
Store
42+
- Startup
43+
│ └- Actions.js
44+
- Theme...
45+
- User
46+
│ ├- Actions.js
47+
│ ├- InitialSate.js
48+
│ ├- Reducers.js
49+
│ └- Selectors.js
50+
- CreateStore.js
51+
- index.js
52+
```
53+
54+
```jsx title="V2"
55+
Services
56+
- User
57+
│ ├- FetchOne.js
58+
│ └- index.js
59+
- index.js
60+
Store
61+
- Startup
62+
│ ├- index.js
63+
│ └- Init.js
64+
- Theme...
65+
- User
66+
│ ├- FetchOne.js
67+
│ └- index.js
68+
- index.js
69+
```
70+
71+
### Configure store
72+
73+
Thanks to a refactoring and redux-toolkit, the store configuration is now in one file easy to understand and flipper debugging ready.
74+
75+
#### V1
76+
77+
```jsx title="V1 App/Stores/CreateStore.js"
78+
import { applyMiddleware, compose, createStore } from 'redux'
79+
import createSagaMiddleware from 'redux-saga'
80+
import { persistReducer, persistStore } from 'redux-persist'
81+
import storage from 'redux-persist/lib/storage'
82+
83+
const persistConfig = {
84+
key: 'root',
85+
storage: storage,
86+
blacklist: [
87+
// 'auth',
88+
],
89+
}
90+
91+
export default (rootReducer, rootSaga) => {
92+
const middleware = []
93+
const enhancers = []
94+
95+
// Connect the sagas to the redux store
96+
const sagaMiddleware = createSagaMiddleware()
97+
middleware.push(sagaMiddleware)
98+
99+
enhancers.push(applyMiddleware(...middleware))
100+
101+
// Redux persist
102+
const persistedReducer = persistReducer(persistConfig, rootReducer)
103+
104+
const store = createStore(persistedReducer, compose(...enhancers))
105+
const persistor = persistStore(store)
106+
107+
// Kick off the root saga
108+
sagaMiddleware.run(rootSaga)
109+
110+
return { store, persistor }
111+
}
112+
```
113+
```jsx title="V1 App/Stores/index.js"
114+
import { combineReducers } from 'redux'
115+
import configureStore from './CreateStore'
116+
import rootSaga from 'App/Sagas'
117+
import { reducer as ExampleReducer } from './Example/Reducers'
118+
119+
export default () => {
120+
const rootReducer = combineReducers({
121+
/**
122+
* Register your reducers here.
123+
* @see https://redux.js.org/api-reference/combinereducers
124+
*/
125+
example: ExampleReducer,
126+
})
127+
128+
return configureStore(rootReducer, rootSaga)
129+
}
130+
```
131+
132+
#### V2
133+
134+
```jsx title="V2 src/Store/index.js"
135+
import AsyncStorage from '@react-native-async-storage/async-storage'
136+
import { combineReducers } from 'redux'
137+
import {
138+
persistReducer,
139+
persistStore,
140+
FLUSH,
141+
REHYDRATE,
142+
PAUSE,
143+
PERSIST,
144+
PURGE,
145+
REGISTER,
146+
} from 'redux-persist'
147+
import { configureStore } from '@reduxjs/toolkit'
148+
149+
import startup from './Startup'
150+
import user from './User'
151+
import theme from './Theme'
152+
153+
const reducers = combineReducers({
154+
startup,
155+
user,
156+
theme,
157+
})
158+
159+
const persistConfig = {
160+
key: 'root',
161+
storage: AsyncStorage,
162+
whitelist: ['theme'],
163+
}
164+
165+
const persistedReducer = persistReducer(persistConfig, reducers)
166+
167+
const store = configureStore({
168+
reducer: persistedReducer,
169+
middleware: (getDefaultMiddleware) => {
170+
const middlewares = getDefaultMiddleware({
171+
serializableCheck: {
172+
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
173+
},
174+
})
175+
176+
if (__DEV__ && !process.env.JEST_WORKER_ID) {
177+
const createDebugger = require('redux-flipper').default
178+
middlewares.push(createDebugger())
179+
}
180+
181+
return middlewares
182+
},
183+
})
184+
185+
const persistor = persistStore(store)
186+
187+
export { store, persistor }
188+
```
189+
190+
### Example feature
191+
192+
Now, a comparison with a feature example present in the V1 and in V2
193+
194+
#### V1
195+
196+
##### Store
197+
In the boilerplate V1, the creation of the Store goes like this :
198+
199+
- init the state
200+
```jsx title="App/Stores/User/InitialState.js"
201+
export const INITIAL_STATE = {
202+
user: {},
203+
userIsLoading: false,
204+
userErrorMessage: null,
205+
}
206+
```
207+
208+
- creation of actions
209+
210+
```jsx title="App/Stores/User/Actions.js"
211+
import { createActions } from 'reduxsauce'
212+
213+
const { Types, Creators } = createActions({
214+
fetchUser: null,
215+
fetchUserLoading: null,
216+
fetchUserSuccess: ['user'],
217+
fetchUserFailure: ['errorMessage'],
218+
})
219+
220+
export const ExampleTypes = Types
221+
export default Creators
222+
```
223+
224+
- creation of associated reducers
225+
226+
```jsx title="App/Stores/User/Reducers.js"
227+
import { INITIAL_STATE } from './InitialState'
228+
import { createReducer } from 'reduxsauce'
229+
import { ExampleTypes } from './Actions'
230+
231+
export const fetchUserLoading = (state) => ({
232+
...state,
233+
userIsLoading: true,
234+
userErrorMessage: null,
235+
})
236+
237+
export const fetchUserSuccess = (state, { user }) => ({
238+
...state,
239+
user: user,
240+
userIsLoading: false,
241+
userErrorMessage: null,
242+
})
243+
244+
export const fetchUserFailure = (state, { errorMessage }) => ({
245+
...state,
246+
user: {},
247+
userIsLoading: false,
248+
userErrorMessage: errorMessage,
249+
})
250+
251+
export const reducer = createReducer(INITIAL_STATE, {
252+
[ExampleTypes.FETCH_USER_LOADING]: fetchUserLoading,
253+
[ExampleTypes.FETCH_USER_SUCCESS]: fetchUserSuccess,
254+
[ExampleTypes.FETCH_USER_FAILURE]: fetchUserFailure,
255+
})
256+
```
257+
258+
##### Saga
259+
In the boilerplate V1, the creation of the Saga goes like this :
260+
261+
- creation of the saga
262+
```jsx title="App/Sagas/UserSaga.js"
263+
import { put, call } from 'redux-saga/effects'
264+
import ExampleActions from 'App/Stores/Example/Actions'
265+
import { userService } from 'App/Services/UserService'
266+
267+
export function* fetchUser() {
268+
yield put(ExampleActions.fetchUserLoading())
269+
270+
const user = yield call(userService.fetchUser)
271+
if (user) {
272+
yield put(ExampleActions.fetchUserSuccess(user))
273+
} else {
274+
yield put(
275+
ExampleActions.fetchUserFailure('There was an error while fetching user informations.')
276+
)
277+
}
278+
}
279+
```
280+
281+
- listen the saga
282+
```jsx title="App/Sagas/index.js"
283+
import { takeLatest, all } from 'redux-saga/effects'
284+
import { ExampleTypes } from 'App/Stores/Example/Actions'
285+
import { StartupTypes } from 'App/Stores/Startup/Actions'
286+
import { fetchUser } from './ExampleSaga'
287+
import { startup } from './StartupSaga'
288+
289+
export default function* root() {
290+
yield all([
291+
takeLatest(StartupTypes.STARTUP, startup),
292+
takeLatest(ExampleTypes.FETCH_USER, fetchUser), // Add this line
293+
])
294+
}
295+
```
296+
297+
298+
##### Service
299+
In the boilerplate V1, the creation of the Service goes like this :
300+
301+
```jsx title="App/Services/UserService.js"
302+
import axios from 'axios'
303+
import { Config } from 'App/Config'
304+
import { is, curryN, gte } from 'ramda'
305+
306+
const isWithin = curryN(3, (min, max, value) => {
307+
const isNumber = is(Number)
308+
return isNumber(min) && isNumber(max) && isNumber(value) && gte(value, min) && gte(max, value)
309+
})
310+
const in200s = isWithin(200, 299)
311+
312+
const userApiClient = axios.create({
313+
baseURL: Config.API_URL,
314+
headers: {
315+
Accept: 'application/json',
316+
'Content-Type': 'application/json',
317+
},
318+
timeout: 3000,
319+
})
320+
321+
function fetchUser() {
322+
const number = Math.floor(Math.random() / 0.1) + 1
323+
324+
return userApiClient.get(number.toString()).then((response) => {
325+
if (in200s(response.status)) {
326+
return response.data
327+
}
328+
329+
return null
330+
})
331+
}
332+
333+
export const userService = {
334+
fetchUser,
335+
}
336+
```
337+
338+
#### V2
339+
340+
##### Store
341+
In the boilerplate V2 action, initial state and reducers goes like this:
342+
343+
```jsx title="src/Store/User/FetchOne.js"
344+
import {
345+
buildAsyncState,
346+
buildAsyncReducers,
347+
buildAsyncActions,
348+
} from '@thecodingmachine/redux-toolkit-wrapper'
349+
import fetchOneUserService from '@/Services/User/FetchOne'
350+
351+
export default {
352+
initialState: buildAsyncState('fetchOne'),
353+
action: buildAsyncActions('user/fetchOne', fetchOneUserService),
354+
reducers: buildAsyncReducers({
355+
errorKey: 'fetchOne.error',
356+
loadingKey: 'fetchOne.loading',
357+
}),
358+
}
359+
```
360+
361+
```jsx title="src/Store/User/index.js"
362+
import { buildSlice } from '@thecodingmachine/redux-toolkit-wrapper'
363+
import FetchOne from './FetchOne'
364+
365+
const sliceInitialState = { item: {} }
366+
367+
export default buildSlice('user', [FetchOne], sliceInitialState).reducer
368+
```
369+
370+
##### Service
371+
372+
In the boilerplate V2, the creation of the Service goes like this :
373+
374+
```javascript
375+
import api, { handleError } from '@/Services'
376+
377+
export default async (userId) => {
378+
if (!userId) {
379+
return handleError({ message: 'User ID is required' })
380+
}
381+
const response = await api.get(`users/${userId}`)
382+
return response.data
383+
}
384+
```
385+
386+
## I18next
387+
This is a new feature of the boilerplate V2, now it handles internationalization thanks to [i18next](https://www.i18next.com/).
388+
See the documentation about it [here](../3_Guides/3_5_AddALangTranslation.md)
389+
390+
## Flipper
391+
This is a new feature of the boilerplate V2, Flipper is now fully integrate with the redux debugger plugin.
392+
See the documentation about it [here](../3_Guides/3_6_UsingFlipper.md)

0 commit comments

Comments
 (0)