In this section, we will start to analyze how to build a React Native application architecture and support a full Native run preview.

See github for the full code

Welcome to my personal blog

preface

There are already many scaffolding tools, such as Ignite, that support one-click creation of a React Native App project structure, which is very convenient, but enjoy the convenience at the same time, also miss the opportunity to learn a complete project architecture and technology stack, and often scaffolding to create application technology architecture does not fully meet our business needs. We need to modify and refine it ourselves, so it’s best to understand a project from zero to one if you want to have more control over the project architecture.

Project structure and technology stack

Use the React Native cli tool to create a React Native application:

react-native init fucCopy the code

The generated project structure is shown as follows:

The initial structure of an RN project

  1. Andorid and ios directories respectively store the corresponding native platform code;
  2. package.jsonFor project dependency management files;
  3. index.ios.jsIs the ios platform entry file,index.android.jsThis is the Android platform entry file, which is usually used to register the React Native App root component.
  4. .babelrcReact Native uses Babel to compile JavaScript code by default.
  5. __tests__Project test directory.

We see that there is no directory for storing the React Native JavaScript code, so we need to create our own directory. Usually we create a SRC directory as the root directory for all the code and resources in the JavaScript part of the App. A SRC /constants directory to hold global shared constant data, a SRC /config directory to hold global configuration, a SRC /helpers directory to hold global helpers, utility class methods, a SRC /app.js entry file as part of RN, In addition, it is often necessary to create directories for redux of each module, directories for REdux middleware, and so on.

Technology stack

The construction of the project architecture largely depends on the technology stack of the project, so the whole technology stack is analyzed first and summarized as follows:

  1. React Native + React library is the premise of the project
  2. App navigation (different routing concepts from the React App)
  3. Application state management containers
  4. Whether Immutable data is required
  5. Persistence of application state
  6. Asynchronous Task Management
  7. Test and helper tools or functions
  8. Develop debugging tools

According to the above division, the following third-party libraries and tools are selected to form the complete technology stack of the project:

  1. React native + react;
  2. React-navigation manages app navigation;
  3. Redux acts as a JavaScript state container. React-redux connects react Native applications to Redux.
  4. Immutable. Js supports Immutable states. Redux-immutable makes the entire Redux store state tree Immutable.
  5. Use redux-persist to support persistence of the redux state tree, and add the redux-persist-immutable extension to support persistence of the IMMUTABLE state tree.
  6. Redux-saga is used to manage asynchronous tasks within the application, such as network requests and asynchronous reading of local data.
  7. Integrate application testing with JEST, use optional helper classes like LoDash, Ramda, and utility class libraries;
  8. Use the Reactotron debugging tool

In view of the above analysis, the improved project structure is shown as follows:

RN project structure

As shown in the figure above, create a SRC directory under the root of the project. In the SRC directory, create 12 directories and 1 React Native part entry JS file.

Develop debugging tools

React Native App development currently has many debugging tools, such as Atom and Nuclide, mobile emulators built-in debugging tools, Reactron, etc.

Nuclide

Nuclide is an Atom-based integrated development environment provided by Facebook for writing, running, and debugging React Native applications.

Simulator debugging tool

After is up and running in the simulator App, the browser will automatically open the http://localhost:8081/debugger-ui page, can be js debug output in the console and distal js breakpoint debugging; The debugging tools can be opened by using the shortcut keys command and D in the simulator terminal, including reloading the application, enabling hot loading, switching DOM viewer, etc. :

RN applies debugging tools

Reactotron

Reactotron is a desktop application for cross-platform debugging of React and React Native applications. It can dynamically monitor and output redux, Action, saga and other asynchronous requests of React applications in real time, as shown in the figure below:

Reactotron

Initialize the reactotron-related configuration first:

import Config from './DebugConfig';

import Immutable from 'immutable';

import Reactotron from 'reactotron-react-native';

import { reactotronRedux as reduxPlugin } from 'reactotron-redux';

import sagaPlugin from 'reactotron-redux-saga';



if (Config.useReactotron) {

  // refer to https://github.com/infinitered/reactotron for more options!

  Reactotron

    .configure({ name: 'Os App' })

    .useReactNative()

    .use(reduxPlugin({ onRestore: Immutable }))

    .use(sagaPlugin())

    .connect();



  // Let's clear Reactotron on every time we load the app

  Reactotron.clear();



  // Totally hacky, but this allows you to not both importing reactotron-react-native

  // on every file.  This is just DEV mode, so no big deal.

  console.tron = Reactotron;

}Copy the code

Then use the console.tron. Overlay method to extend the portal component:

import './config/ReactotronConfig';

import DebugConfig from './config/DebugConfig';



class App extends Component {

  render () {

    return (

      <Provider store={store}>

        <AppContainer />

      </Provider>

    )

  }

}



// allow reactotron overlay for fast design in dev mode

export default DebugConfig.useReactotron

  ? console.tron.overlay(App)

  : AppCopy the code

At this point, you can use the Reactotron client to capture all redux and actions initiated in your application.

components

React Native applications still follow the React component-based development principle. Components are responsible for rendering the UI. Different states of components correspond to different UIs.

  1. Layout component: only involves the application of THE UI interface structure of the component, does not involve any business logic, data requests and operations;
  2. Container components: responsible for fetching data, processing business logic, and usually returning presentation components within the Render () function;
  3. Display component: responsible for application interface AND UI display;
  4. UI component: The abstraction of reusable UI independent components, usually stateless components;
Display component Container components
The target UI presentation (HTML structure and style) Business logic (get data, update status)
Perception Redux There is no There are
The data source props Subscription Redux store
Change data Call the callback function passed by props Dispatch Redux actions
reusable Independence is strong The service coupling is high

Cross-platform adaptation

React Native does a lot of cross-platform compatibility when creating cross-platform applications, but there are still some situations where you need to develop different code for different platforms, which requires extra handling.

Cross-platform directory

We can separate platform code files in different directories, for example:

/common/components/
/android/components/
/ios/components/Copy the code

The Common directory stores public files, the Android directory stores Android file code, and the ios directory stores ios file code. However, React Native is usually a better option.

Platform module

React Native has a built-in Platform module that identifies the Platform on which an application is running. When running on an ios Platform, platform. OS is set to ios, and when running on an Android Platform, it is set to Android.

var StatusBar = Platform.select({

  ios: () => require('ios/components/StatusBar'),

  android: () => require('android/components/StatusBar'),

}) ();Copy the code

Then use the StatusBar component as normal.

React Native platform detection

When a component is referenced, React Native checks if the file has an. Android or. Ios suffix. If it does, it loads the component based on the current platform, for example:

StatusBar.ios.js
StatusBar.indroid.jsCopy the code

If the preceding two files exist in the same directory, you can reference them in the following way:

import StatusBar from './components/StatusBar';Copy the code

React will load the corresponding suffix file based on the current Platform. It is recommended to use this method for Platform component-level code adaptation. For partial and small parts of code that need to be adapted to the Platform, platform. OS values can be used as follows:

var styles = StyleSheet.create({

marginTop: (Platform.OS === 'ios') ? 10, 20:

});Copy the code

App navigation and routing

Unlike single-page routing in React applications, React Native usually exists in the form of multiple pages and switches between different pages and components in a navigation mode, rather than controlling the display of different components in a routing mode. The most commonly used navigation library is React-Navigation.

Navigation and Routing

In the React Web application, the display and switching of UI components are completely controlled by routes. Each route has its corresponding URL and routing information. In the React Native application, it is not displayed by the route driven component, but by the navigation control switching screen, each screen has its own routing information.

You may already rely on the React-Router single-page application routing configuration and want to create a URL-driven cross-platform App. For active open source communities, you can use react-router-native, but it is not recommended. It is better to use multiple pages (screens).

react-navigation

React-navigation allows you to define cross-platform application navigation structures and configure and render cross-platform navigation bar, TAB bar and other components.

Built-in navigation module

React-navigation provides the following methods to create different navigation types:

  1. StackNavigator: Create a stack of navigation screens where all screens exist as stacks, render one screen at a time, enhance the transform animation when switching screens, and place a screen at the top of the stack when opening one;
  2. TabNavigator: Create a tab-based navigation and render a Tab menu bar that allows the user to switch between different screens.
  3. DrawerNavigator: Create a drawer navigation that slides out from the left side of the screen.

StackNavigator

StackNavigator supports switching screens across platforms and placing the current screen at the top of the stack as follows:

StackNavigator(RouteConfigs, StackNavigatorConfig)Copy the code
RouteConfigs

The navigation stack Route (Route) configuration object defines the route name and the Route object. The Route object defines the display component corresponding to the current route, such as:

// routes indicates the routing information object

StackNavigator({

  [routes.Main.name]: Main,

  [routes.Login.name]: {

    path: routes.Login.path,

    screen: LoginContainer,

    title: routes.Login.title

  }

}Copy the code

As shown above, when the application navigates to routes.login. name, the LoginContainer component is rendered, as specified by the screen property of the object. When navigating to the routes.main. name value, MainStack is rendered.Main object in the code is:

{

  path: routes.Main.path,

  screen: MainStack,

  navigationOptions: {

    gesturesEnabled: false,

  },

}Copy the code

MainStack is a Stacknavigator:

const MainStack = StackNavigator({

  Home: HomeTabs

})Copy the code

HomeTabs is a TabNavigator:

{

  name: 'Home Tabs',

  description: 'Tabs following Home Tabs',

  headerMode: 'none',

  screen: HomeTabs

};Copy the code
StackNavigatorConfig

Route configuration object, which can be optionally configured with optional properties, such as:

  1. initialRouteName, the default screen of the initial navigation stack, must be a key name in the routing configuration object;
  2. initialRouteParamsIs the default parameter of the initial route.
  3. navigationOptionsTo set the default navigation screen configuration.
    1. Title: navigation screen top title;
  4. headerMode, whether to display the top navigation bar:
    1. None: The navigation bar is not displayed.
    2. Float: Render a separate navigation bar at the top, and animate as you switch screens, usually in ios display mode;
    3. Screen: Bind a navigation bar to each screen and fade in and out with screen switching, usually in Android display mode;
  5. mode, the style and transformation effect of navigation switching screen:
    1. card: Default mode, standard screen transform;
    2. modal: Works only on ios platforms, making a new screen slide at the bottom of the screen.
{

  initialRouteName: routes.Login.name,

HeaderMode: 'None ', // Remove the top navigation bar

/ * *

   * Use modal on iOS because the card mode comes from the right,

   * which conflicts with the drawer example gesture

* /

  mode: Platform.OS === 'ios' ? 'modal' : 'card'

}Copy the code

TabNavigator

With TabNavigator, you can create a screen. With TabRouter, you can switch between different tabs.

TabNavigator(RouteConfigs, TabNavigatorConfig)Copy the code
RouteConfigs

Tab routing configuration object in a format similar to StackNavigator.

TabNavigatorConfig

Tab navigation configuration objects, such as:

  1. TabBarComponent: A component used by the TAB menu bar. It is used by default on iosTabBarBottomComponent used by default on the Android platformTabBarTopComponents;
  2. TabBarPosition: Position of the TAB menu bar,toporbottom;
  3. TabBarOptions: TAB menu bar configuration:
    1. ActiveTintColor: Activates the TAB’s menu bar item’s font and icon’s color
  4. InitialRouteName: Indicates the routeName of the default tabRoute route during initial loading, corresponding to the key name of the route configuration object
  5. Order: TAB sort, routeName array;
const HomeTabs = TabNavigator(

  {

    Notification: {

      screen: NotificationTabContainer,

      path: 'notification',

      navigationOptions: {

Title: 'Message notification'

      }

    },

    Myself: {

      screen: MyselfTabContainer,

      path: 'myself',

      navigationOptions: {

Title: 'Mine'

      }

    }

  },

  {

    tabBarOptions: {

      activeTintColor: Platform.OS === 'ios' ? '#e91e63' : '#fff',

    },

    swipeEnabled: true

  }

);Copy the code

DrawerNavigator

Use DrawerNavigator to create a drawer navigation screen as follows:

DrawerNavigator(RouteConfigs, DrawerNavigatorConfig)Copy the code
const MyDrawer = DrawerNavigator({

  Home: {

    screen: MyHomeDrawerScreen,

  },

  Notifications: {

    screen: MyNotificationsDrawerScreen,

  },

});Copy the code
RouteConfigs

Drawer navigation routing configuration object, similar to StackNavigator format.

DrawerNavigatorConfig

Drawer type navigation screen configuration object, such as:

  1. DrawerWidth: the width of the drawer screen;
  2. DrawerPosition: position of drawer screen,leftorright;
  3. ContentComponent: drawer screen contentComponent, as provided internallyDrawerItems;
  4. InitialRouteName: indicates the name of the initial route.
import { DrawerItems } from 'react-navigation';



const CustomDrawerContentComponent = (props) => (

  <View style={styles.container}>

<DrawerItems {... props} />

  </View>

);



const DrawerNavi = DrawerNavigator({}, {

  drawerWidth: 200,

  drawerPosition: 'right',

contentComponent: props => <CustomDrawerContentComponent {... props}/>,

  drawerBackgroundColor: 'transparent'

})Copy the code

Navigation prop

Each screen of an RN application will receive a Navigation property containing the following methods and properties:

  1. Navigate: an aid method to navigate to another screen;
  2. SetParams: change route parameter method;
  3. GoBack: Close the current screen and back;
  4. State: indicates the status or routing information of the current screen.
  5. Dispatch: Release action;
navigate

Navigate to another screen using the navigate method:

navigate(routeName, params, action)Copy the code
  1. RouteName: indicates the name of the destination route, which is the route key registered in the App navigation route.
  2. Params: parameters carried by the destination route.
  3. Action: If the destination route has child routes, this action is executed in the child routes.
setParams

Changing the current navigation route information, such as setting and modifying navigation title:

class ProfileScreen extends React.Component {

  render() {

    const { setParams } = this.props.navigation;

    return (

      <Button

        onPress={() => setParams({name: 'Jh'})}

        title="Set title"

      />

     )

   }

}Copy the code
goBack

Navigate from the current screen (the parameter is empty) or the specified screen (the parameter is the routing key name) to the previous screen and close the screen. If null is passed, the source screen is not specified, that is, the screen is not closed.

state

Each screen has its own routing information, can use this. Props. Navigation. The state visit, the returned data formats such as:

{

  // the name of the route config in the router

  routeName: 'Login',

  //a unique identifier used to sort routes

  key: 'login',

  //an optional object of string options for this screen

  params: { user: 'jh' }

}Copy the code
dispatch

NavigationActions (NavigationActions); / / Assign a navigate action to the route. Navigate with NavigationActions (NavigationActions); / / Assign a navigate action to the route.

import { NavigationActions } from 'react-navigation'



const navigateAction = NavigationActions.navigate({

  routeName: routeName || routes.Login.name,

  params: {},

  // navigate can have a nested navigate action that will be run inside the child router

  action: NavigationActions.navigate({ routeName: 'Notification'})

});



// dispatch the action

this.props.navigation.dispatch(navigateAction);Copy the code

Navigation and story

After using Redux, you need to follow the Redux principles: Single trusted data source, i.e. all data sources can only be REudx store, and the routing state of Navigation should not be an exception, so it is necessary to connect Navigation state with store state. We can create a Navigation Reducer to merge Navigation state into store:

import AppNavigation from '.. /routes';



const NavigationReducer = (state = initialState, action) => {

  const newState = Object.assign({}, state, AppNavigation.router.getStateForAction(action, state));

  return newState || state;

};



export const NavigationReducers = {

  nav: NavigationReducer

};Copy the code

All this reducer does is merge the App navigation route state into the store.

Redux

If there is no state management container for any large modern Web application, it will lack the characteristics of The Times. Optional libraries, such as Mobx and Redux, are virtually the same and different. Take Redux for example, redux is the most commonly used React application state container library, which is also applicable to React Native applications.

react-redux

Like the React application, Redux and applications need to be connected so that Redux can be used to manage application status in a unified manner. Use the official React-Redux library.

class App extends Component {

  render () {

    return (

      <Provider store={store}>

        <AppContainer />

      </Provider>

    )

  }

}Copy the code

createStore

Create a Redux Store using the createStore method provided by Redux, but in real projects we often need to extend Redux to add some custom features or services, such as Adding Redux middleware, adding asynchronous task management saga, enhancing Redux, etc. :

// creates the store

export default (rootReducer, rootSaga, initialState) => {

  /* ------------- Redux Configuration ------------- */

  const middleware = [];

  const enhancers = [];



  /* ------------- Analytics Middleware ------------- */

  middleware.push(ScreenTracking);



  /* ------------- Saga Middleware ------------- */

  const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;

  const sagaMiddleware = createSagaMiddleware({ sagaMonitor });

  middleware.push(sagaMiddleware);



  /* ------------- Assemble Middleware ------------- */

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



  /* ------------- AutoRehydrate Enhancer ------------- */

  // add the autoRehydrate enhancer

  if (ReduxPersist.active) {

    enhancers.push(autoRehydrate());

  }



  // if Reactotron is enabled (default for __DEV__), 

  // we'll create the store through Reactotron

  const createAppropriateStore = Config.useReactotron ? console.tron.createStore : createStore;

const store = createAppropriateStore(rootReducer, initialState, compose(... enhancers));



  // configure persistStore and check reducer version number

  if (ReduxPersist.active) {

    RehydrationServices.updateReducers(store);

  }



  // kick off root saga

  sagaMiddleware.run(rootSaga);



  return store;

}Copy the code

Story and Immutable

Redux provides the combineReducers method to integrate Reduers into Redux by default. However, this default method expects to accept native JavaScript objects and it handles state as a native object. So when we use the createStore method and accept an Immutable object for the initial state, reducer will return an error. The source code is as follows:

if (! isPlainObject(inputState)) {

    return   (                              

        `The   ${argumentName} has unexpected type of "` +                                    ({}).toString.call(inputState).match(/\s([a-z|A-Z]+)/)[1] +

      ".Expected argument to be an object with the following + 

      `keys:"${reducerKeys.join('", "')}"`   

    )  

}Copy the code

As indicated above, the state parameter accepted by the original reducer type should be a native JavaScript object. We need to enhance combineReducers to be able to handle Immutable objects. Redux-immutable creates a Redux combineReducers that can work with immutable.

import { combineReducers } from 'redux-immutable';

import Immutable from 'immutable';

import configureStore from './CreateStore';



// use Immutable.Map to create the store state tree

const initialState = Immutable.Map();



export default () => {

  // Assemble The Reducers

  const rootReducer = combineReducers({

. NavigationReducers,

. LoginReducers

  });



  return configureStore(rootReducer, rootSaga, initialState);

}Copy the code

As you can see from the code above, the initialState we passed in is Immutable.Map data, we Immutable the entire state tree root of Redux, and we passed in reducers and Sagas that handle Immutable state.

Each state tree node data is Immutable, as shown in NavigationReducer:

const initialState = Immutable.fromJS({

  index: 0,

  routes: [{

    routeName: routes.Login.name,

    key: routes.Login.name

  }]

});



const NavigationReducer = (state = initialState, action) => {

  const newState = state.merge(AppNavigation.router.getStateForAction(action, state.toJS()));

  return newState || state;

};Copy the code

The default state nodes are transformed into Immutable structures using the immutable.fromjs () method, and the Immutable method state.merge() is used when state is updated to ensure uniform and predictable state.

Redux persistence

As we know, browsers have the caching function of resources by default and provide local persistent storage methods, such as localStorage, indexDb, webSQL, etc. Usually, some data can be stored locally. When users access it again within a certain period, they can directly recover data from the local, which can greatly improve the application startup speed. User experience is more advantageous. For App applications, local persistence of some startup data or even offline applications is a common requirement. We can use AsyncStorage (similar to Web localStorage) to store some data, and SQLite can be used for large data storage.

In addition, different from the previous direct data storage, data is read locally and then recovered when the application is started. For redux application, if only data is stored, then we have to expand each reducer and read persistent data when the application is started again, which is a cumbersome and inefficient way. Can I try to save the Reducer key and then restore the persistent data based on the key? First register the Rehydrate Reducer, restore the data based on the Reducer key when actions are triggered, and then only distribute the actions when the application starts. This could easily be abstracted into a configurable extension service, and in fact the three-party library Redux-Persist already does this for us.

redux-persist

To implement redux persistence, including the local persistent storage of Redux store and recovery startup, if you write your own implementation, the amount of code is complicated, you can use the open source library Redux-persist. It provides the persistStore and autoRehydrate methods to persist the local store and recover the launch store, respectively, as well as support for custom incoming persistence and conversion and extension of store state when recovering the store.

The persistent store

The following when creating store called when persistStore related services – RehydrationServices. UpdateReducers () :

// configure persistStore and check reducer version number

if (ReduxPersist.active) {

  RehydrationServices.updateReducers(store);

}Copy the code

This method implements persistent store:

// Check to ensure latest reducer version

AsyncStorage.getItem('reducerVersion').then((localVersion) => {

if (localVersion ! == reducerVersion) {

    if (DebugConfig.useReactotron) {

      console.tron.display({

        name: 'PURGE',

        value: {

          'Old Version:': localVersion,

          'New Version:': reducerVersion

        },

        preview: 'Reducer Version Change Detected',

        important: true

      });

    }

    // Purge store

    persistStore(store, config, startApp).purge();

    AsyncStorage.setItem('reducerVersion', reducerVersion);

  } else {

    persistStore(store, config, startApp);

  }

}).catch(() => {

  persistStore(store, config, startApp);

  AsyncStorage.setItem('reducerVersion', reducerVersion);

})Copy the code

A Reducer version number will be stored in the AsyncStorage, which can be configured in the application configuration file. This reducer version number and store will be stored when the reducer version number is first implemented. If the reducer version number changes, the original store will be emptied. Otherwise, pass a store to the persistence method persistStore.

persistStore(store, [config, callback])Copy the code

The approach mainly implements store persistence and distribution of rehydration action:

  1. Subscribe to the Redux Store and trigger store store operations when it changes;
  2. Take the data from the specified StorageEngine (such as AsyncStorage), transform it, and trigger the REHYDRATE process by distributing the REHYDRATE Action;

The receiving parameters are as follows:

  1. Store: persistent store;
  2. Config: indicates the configuration object
    1. Storage: a persistence engine, such as LocalStorage and AsyncStorage;
    2. Transforms: Transforms called during the Rehydration and storage phases;
    3. Blacklist: specifies the key of the reducers that can be persistently ignored.
  3. Callback: The callback after the end of Ehydration operation;

resume

As with persisStore, the Rehydrate extension was initially registered when the Redux Store was created:

// add the autoRehydrate enhancer

if (ReduxPersist.active) {

  enhancers.push(autoRehydrate());

}Copy the code

This method does a simple job of using a persistent data recovery rehydrate store, which is a reducer registered with an autoRehydarte reducer that receives rehydrate actions distributed by the persistStore method, Then merge state.

Of course, autoRehydrate is not required and we can customize the recovery store:

import {REHYDRATE} from 'redux-persist/constants';



/ /...

case REHYDRATE:

  const incoming = action.payload.reducer

  if (incoming) {

    return {

. state,

. incoming

    }

  }

  return state;Copy the code

Version update

Note that the Redux-Persist library has been shipped to V5.x, and this article is based on V4.x.

Persistence and Immutable

As mentioned earlier, redux-persist can only handle Redux store state of native JavaScript objects by default, so it needs to be extended to be compatible with Immutable.

redux-persist-immutable

It is easy to achieve compatibility using the Redux-persist -immutable library by replacing the method provided by Redux-persist with the persistStore method it provides:

import { persistStore } from 'redux-persist-immutable';Copy the code

transform

We know that a persistent store is best for native JavaScript objects, because Immutable data usually contains a lot of auxiliary information and is not easy to store. Therefore, we need to define the transformation operations for persisting and recovering data:

import R from 'ramda';

import Immutable, { Iterable } from 'immutable';



// change this Immutable object into a JS object

const convertToJs = (state) => state.toJS();



// optionally convert this object into a JS object if it is Immutable

const fromImmutable = R.when(Iterable.isIterable, convertToJs);



// convert this JS object into an Immutable object

const toImmutable = (raw) => Immutable.fromJS(raw);



// the transform interface that redux-persist is expecting

export default {

  out: (state) => {

    return toImmutable(state);

  },

  in: (raw) => {

    return fromImmutable(raw);

  }

};Copy the code

As shown above, in and out in output objects correspond to transformations for persisting and recovering data, respectively. This is done by converting Js and Immutable data structures using fromJS() and toJS() as follows:

import immutablePersistenceTransform from '.. /services/ImmutablePersistenceTransform'

persistStore(store, {

  transforms: [immutablePersistenceTransform]

}, startApp);Copy the code

Immutable

When you introduce Immutable into your project, you should try to ensure that:

  1. Uniform Immutable for the entire state tree of redux Store
  2. Redux persistence compatibility with Immutable data;
  3. App Navigation compatible with Immutable;

Immutable and App Navigation

The third point is that Navigation is compatible with Immutable, which is to make Navigation routing state compatible with Immutable. In the App Navigation and Routing section, we have explained how to connect the Navigation router state to the Redux store. If the application uses the Immutable library, we need to change the Navigation router state to Immutable. Modify the NavigationReducer mentioned earlier:

const initialState = Immutable.fromJS({

  index: 0,

  routes: [{

    routeName: routes.Login.name,

    key: routes.Login.name

  }]

});



const NavigationReducer = (state = initialState, action) => {

  const newState = state.merge(AppNavigation.router.getStateForAction(action, state.toJS()));

  return newState || state;

};Copy the code

Convert the default initial state to Immutable, and merge state using the merge() method.

Asynchronous task flow management

Finally, the module to be introduced is asynchronous task management. In the process of application development, the most important asynchronous task is data HTTP request, so we talk about asynchronous task management, mainly focusing on the process management of data HTTP request.

axios

This project uses AXIos as the HTTP request library. Axios is an HTTP client in the Promise format. This library was chosen for several reasons:

  1. Can initiate XMLHttpRequest in the browser, can also initiate HTTP requests in node.js side;
  2. Supporting Promise;
  3. Intercepting requests and responses;
  4. Can cancel the request;
  5. Automatically convert JSON data;

redux-saga

Redux-saga is a tripartite library dedicated to making asynchronous tasks such as data fetching and local cache access easier to manage, run efficiently, test and handle exceptions.

Redux-saga is a Redux middleware. It is like a single process in the application, only responsible for managing asynchronous tasks. It can accept Redux actions from the main application process to decide whether to start, suspend, or cancel the process task. Then distribute the action.

Initialize the saga

Redux-saga is a middleware, so first call the createSagaMiddleware method to create the middleware, then use Redux’s applyMiddleware method to enable the middleware, and then use the Compose helper method to pass to createStore to create the store. Finally, call the run method to start root saga:

import { createStore, applyMiddleware, compose } from 'redux';

import createSagaMiddleware from 'redux-saga';

import rootSaga from '.. /sagas/'



const sagaMiddleware = createSagaMiddleware({ sagaMonitor });

middleware.push(sagaMiddleware);

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



const store = createStore(rootReducer, initialState, compose(... enhancers));



// kick off root saga

sagaMiddleware.run(rootSaga);Copy the code

Saga shunt

There are usually many parallel modules in a project, and the saga flow of each module should also be parallel, in the form of multiple branches. The fork method provided by Redux-Saga is to start the current saga flow in the form of a new branch:

import { fork, takeEvery } from 'redux-saga/effects';

import LoginSagas from './LoginSagas';



const sagas = [

. LoginSagas,

. StartAppSagas

];



export default function * root() {

  yield sagas.map(saga => fork(saga)); 

};Copy the code

As above, first collect all module root saga, then iterate through the number group and start each saga to stream root saga.

Saga instance

LoginSagas, for example, may require asynchronous requests after the user logs in, so loginSaga and loginSuccessSaga may also require HTTP requests when the user logs out of the account. So put logoutSaga here:

.



// process login actions

export function * loginSaga () {

  yield takeLatest(LoginTypes.LOGIN, login);

}



export function * loginSuccessSaga () {

  yield takeLatest(LoginTypes.LOGIN_SUCCESS, loginSuccess);

}



export function * logoutSaga () {

  yield takeLatest(LogoutTypes.LOGOUT, logout);

}



const sagas = [

  loginSaga,

  loginSuccessSaga,

  logoutSaga

];



export default sagas;Copy the code

LOGINaction, when receiving this action, calls login, which is essentially a saga, which handles asynchronous tasks:

function * login (action) {

  const { username, password } = action.payload || {};



  if (username && password) {

    const res = yield requestLogin({

      username,

      password

    });



    const { data } = res || {};



    if (data && data.success) {

      yield put(LoginActions.loginSuccess({

        username,

        password,

        isLogin: true

      }));

    } else {

      yield put(LoginActions.loginFail({

        username,

        password,

        isLogin: false

      }));

    }

  } else {

    yield put(LoginActions.loginFail({

      username,

      password,

      isLogin: false

    }));

  }

}Copy the code

The requestLogin method is a login HTTP request. The username and password parameters are retrieved from the load passed by the LoginTypes.LOGINaction.

  1. Login successful. DistributeLoginActions.loginSuccessAction, and then the Reducer and listen on this actionloginSuccessSagasaga;
  2. Logon failed. DistributionLoginActions.loginFailaction;

Put is a distributable action method provided by Redux-Saga.

Saga with Reactotron

Redux-saga is a kind of REdux middleware, so capturing sagas requires additional configuration. When creating store, add sagaMonitor service to saga middleware to listen to saga:

const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;

const sagaMiddleware = createSagaMiddleware({ sagaMonitor });

middleware.push(sagaMiddleware);

.Copy the code

conclusion

This paper summarizes in detail the process of building a project architecture from 0 to 1, and has a deeper understanding and thinking of React, React Native, Redux application and project engineering practice, so as to continue to forge ahead on the road of big front-end growth.

See github for the full code

reference

  1. react native
  2. React Native Chinese
  3. react navigation