Skip to main content

Migrating from V1 ๐Ÿ‘ด

For the V2, our code of conduct is "keep the code simple and concise" ๐Ÿค“

Reduxโ€‹

After the redux-toolkit release, and understood that we don't need the power, and the large among of functionalities that Redux Saga provides, we conclude that, because we want simple and concise code, we will now use redux-toolkit.

๐Ÿšจ๏ธ 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. ๐Ÿšจ๏ธ

LibrariesV1V2Goal
reduxโœ…โœ…State management
redux sagaโœ…โŒRedux middleware
redux sauceโœ…โŒSimplify redux syntax
redux-toolkitโŒโœ…New redux library with some function helpers
redux-toolkit-wrapperโŒโœ…Easier CRUD redux-toolkit function helpers

Migration guideโ€‹

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. So, in next sections, there is a structure and code comparison.

Architectureโ€‹

First, a quick comparison on the tree files. On V1 the state management logic was divide in Services, Sagas and Store. 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.

V1
Services
โ””- UserService.js
Sagas
โ”œ- UserSaga.js
โ”œ- StartupSaga.js
โ””- index.js
Store
โ”œ- Startup
โ”‚ โ””- Actions.js
โ”œ- Theme...
โ”œ- User
โ”‚ โ”œ- Actions.js
โ”‚ โ”œ- InitialSate.js
โ”‚ โ”œ- Reducers.js
โ”‚ โ””- Selectors.js
โ”œ- CreateStore.js
โ””- index.js
V2
Services
โ”œ- User
โ”‚ โ”œ- FetchOne.js
โ”‚ โ””- index.js
โ””- index.js
Store
โ”œ- Startup
โ”‚ โ”œ- index.js
โ”‚ โ””- Init.js
โ”œ- Theme...
โ”œ- User
โ”‚ โ”œ- FetchOne.js
โ”‚ โ””- index.js
โ””- index.js

Configure storeโ€‹

Thanks to a refactoring and redux-toolkit, the store configuration is now in one file easy to understand and flipper debugging ready.

V1โ€‹

V1 App/Stores/CreateStore.js
import { applyMiddleware, compose, createStore } from 'redux'
import createSagaMiddleware from 'redux-saga'
import { persistReducer, persistStore } from 'redux-persist'
import storage from 'redux-persist/lib/storage'

const persistConfig = {
key: 'root',
storage: storage,
blacklist: [
// 'auth',
],
}

export default (rootReducer, rootSaga) => {
const middleware = []
const enhancers = []

// Connect the sagas to the redux store
const sagaMiddleware = createSagaMiddleware()
middleware.push(sagaMiddleware)

enhancers.push(applyMiddleware(...middleware))

// Redux persist
const persistedReducer = persistReducer(persistConfig, rootReducer)

const store = createStore(persistedReducer, compose(...enhancers))
const persistor = persistStore(store)

// Kick off the root saga
sagaMiddleware.run(rootSaga)

return { store, persistor }
}
V1 App/Stores/index.js
import { combineReducers } from 'redux'
import configureStore from './CreateStore'
import rootSaga from 'App/Sagas'
import { reducer as ExampleReducer } from './Example/Reducers'

export default () => {
const rootReducer = combineReducers({
/**
* Register your reducers here.
* @see https://redux.js.org/api-reference/combinereducers
*/
example: ExampleReducer,
})

return configureStore(rootReducer, rootSaga)
}

V2โ€‹

V2 src/Store/index.js
import AsyncStorage from '@react-native-async-storage/async-storage'
import { combineReducers } from 'redux'
import {
persistReducer,
persistStore,
FLUSH,
REHYDRATE,
PAUSE,
PERSIST,
PURGE,
REGISTER,
} from 'redux-persist'
import { configureStore } from '@reduxjs/toolkit'

import startup from './Startup'
import user from './User'
import theme from './Theme'

const reducers = combineReducers({
startup,
user,
theme,
})

const persistConfig = {
key: 'root',
storage: AsyncStorage,
whitelist: ['theme'],
}

const persistedReducer = persistReducer(persistConfig, reducers)

const store = configureStore({
reducer: persistedReducer,
middleware: (getDefaultMiddleware) => {
const middlewares = getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
})

if (__DEV__ && !process.env.JEST_WORKER_ID) {
const createDebugger = require('redux-flipper').default
middlewares.push(createDebugger())
}

return middlewares
},
})

const persistor = persistStore(store)

export { store, persistor }

Example featureโ€‹

Now, a comparison with a feature example present in the V1 and in V2

V1โ€‹

Storeโ€‹

In the boilerplate V1, the creation of the Store goes like this :

  • init the state
App/Stores/User/InitialState.js
export const INITIAL_STATE = {
user: {},
userIsLoading: false,
userErrorMessage: null,
}
  • creation of actions
App/Stores/User/Actions.js
import { createActions } from 'reduxsauce'

const { Types, Creators } = createActions({
fetchUser: null,
fetchUserLoading: null,
fetchUserSuccess: ['user'],
fetchUserFailure: ['errorMessage'],
})

export const ExampleTypes = Types
export default Creators
  • creation of associated reducers
App/Stores/User/Reducers.js
import { INITIAL_STATE } from './InitialState'
import { createReducer } from 'reduxsauce'
import { ExampleTypes } from './Actions'

export const fetchUserLoading = (state) => ({
...state,
userIsLoading: true,
userErrorMessage: null,
})

export const fetchUserSuccess = (state, { user }) => ({
...state,
user: user,
userIsLoading: false,
userErrorMessage: null,
})

export const fetchUserFailure = (state, { errorMessage }) => ({
...state,
user: {},
userIsLoading: false,
userErrorMessage: errorMessage,
})

export const reducer = createReducer(INITIAL_STATE, {
[ExampleTypes.FETCH_USER_LOADING]: fetchUserLoading,
[ExampleTypes.FETCH_USER_SUCCESS]: fetchUserSuccess,
[ExampleTypes.FETCH_USER_FAILURE]: fetchUserFailure,
})
Sagaโ€‹

In the boilerplate V1, the creation of the Saga goes like this :

  • creation of the saga
App/Sagas/UserSaga.js
import { put, call } from 'redux-saga/effects'
import ExampleActions from 'App/Stores/Example/Actions'
import { userService } from 'App/Services/UserService'

export function* fetchUser() {
yield put(ExampleActions.fetchUserLoading())

const user = yield call(userService.fetchUser)
if (user) {
yield put(ExampleActions.fetchUserSuccess(user))
} else {
yield put(
ExampleActions.fetchUserFailure('There was an error while fetching user informations.')
)
}
}
  • listen the saga
App/Sagas/index.js
import { takeLatest, all } from 'redux-saga/effects'
import { ExampleTypes } from 'App/Stores/Example/Actions'
import { StartupTypes } from 'App/Stores/Startup/Actions'
import { fetchUser } from './ExampleSaga'
import { startup } from './StartupSaga'

export default function* root() {
yield all([
takeLatest(StartupTypes.STARTUP, startup),
takeLatest(ExampleTypes.FETCH_USER, fetchUser), // Add this line
])
}
Serviceโ€‹

In the boilerplate V1, the creation of the Service goes like this :

App/Services/UserService.js
import axios from 'axios'
import { Config } from 'App/Config'
import { is, curryN, gte } from 'ramda'

const isWithin = curryN(3, (min, max, value) => {
const isNumber = is(Number)
return isNumber(min) && isNumber(max) && isNumber(value) && gte(value, min) && gte(max, value)
})
const in200s = isWithin(200, 299)

const userApiClient = axios.create({
baseURL: Config.API_URL,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
timeout: 3000,
})

function fetchUser() {
const number = Math.floor(Math.random() / 0.1) + 1

return userApiClient.get(number.toString()).then((response) => {
if (in200s(response.status)) {
return response.data
}

return null
})
}

export const userService = {
fetchUser,
}

V2โ€‹

Storeโ€‹

In the boilerplate V2 action, initial state and reducers goes like this:

src/Store/User/FetchOne.js
import {
buildAsyncState,
buildAsyncReducers,
buildAsyncActions,
} from '@thecodingmachine/redux-toolkit-wrapper'
import fetchOneUserService from '@/Services/User/FetchOne'

export default {
initialState: buildAsyncState('fetchOne'),
action: buildAsyncActions('user/fetchOne', fetchOneUserService),
reducers: buildAsyncReducers({
errorKey: 'fetchOne.error',
loadingKey: 'fetchOne.loading',
}),
}
src/Store/User/index.js
import { buildSlice } from '@thecodingmachine/redux-toolkit-wrapper'
import FetchOne from './FetchOne'

const sliceInitialState = { item: {} }

export default buildSlice('user', [FetchOne], sliceInitialState).reducer
Serviceโ€‹

In the boilerplate V2, the creation of the Service goes like this :

import api, { handleError } from '@/Services'

export default async (userId) => {
if (!userId) {
return handleError({ message: 'User ID is required' })
}
const response = await api.get(`users/${userId}`)
return response.data
}

I18nextโ€‹

This is a new feature of the boilerplate V2, now it handles internationalization thanks to i18next. See the documentation about it here

Flipperโ€‹

This is a new feature of the boilerplate V2, Flipper is now fully integrate with the redux debugger plugin. See the documentation about it here