|
| 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