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:
- Andorid and ios directories respectively store the corresponding native platform code;
package.json
For project dependency management files;index.ios.js
Is the ios platform entry file,index.android.js
This is the Android platform entry file, which is usually used to register the React Native App root component..babelrc
React Native uses Babel to compile JavaScript code by default.__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:
- React Native + React library is the premise of the project
- App navigation (different routing concepts from the React App)
- Application state management containers
- Whether Immutable data is required
- Persistence of application state
- Asynchronous Task Management
- Test and helper tools or functions
- 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:
- React native + react;
- React-navigation manages app navigation;
- Redux acts as a JavaScript state container. React-redux connects react Native applications to Redux.
- Immutable. Js supports Immutable states. Redux-immutable makes the entire Redux store state tree Immutable.
- 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.
- Redux-saga is used to manage asynchronous tasks within the application, such as network requests and asynchronous reading of local data.
- Integrate application testing with JEST, use optional helper classes like LoDash, Ramda, and utility class libraries;
- Use the Reactotron debugging tool
In view of the above analysis, the improved project structure is shown as follows:
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. :
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:
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.
- Layout component: only involves the application of THE UI interface structure of the component, does not involve any business logic, data requests and operations;
- Container components: responsible for fetching data, processing business logic, and usually returning presentation components within the Render () function;
- Display component: responsible for application interface AND UI display;
- 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:
- 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;
- TabNavigator: Create a tab-based navigation and render a Tab menu bar that allows the user to switch between different screens.
- 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:
initialRouteName
, the default screen of the initial navigation stack, must be a key name in the routing configuration object;initialRouteParams
Is the default parameter of the initial route.navigationOptions
To set the default navigation screen configuration.- Title: navigation screen top title;
headerMode
, whether to display the top navigation bar:- None: The navigation bar is not displayed.
- Float: Render a separate navigation bar at the top, and animate as you switch screens, usually in ios display mode;
- Screen: Bind a navigation bar to each screen and fade in and out with screen switching, usually in Android display mode;
mode
, the style and transformation effect of navigation switching screen:card
: Default mode, standard screen transform;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:
- TabBarComponent: A component used by the TAB menu bar. It is used by default on ios
TabBarBottom
Component used by default on the Android platformTabBarTop
Components; - TabBarPosition: Position of the TAB menu bar,
top
orbottom
; - TabBarOptions: TAB menu bar configuration:
- ActiveTintColor: Activates the TAB’s menu bar item’s font and icon’s color
- InitialRouteName: Indicates the routeName of the default tabRoute route during initial loading, corresponding to the key name of the route configuration object
- 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:
- DrawerWidth: the width of the drawer screen;
- DrawerPosition: position of drawer screen,
left
orright
; - ContentComponent: drawer screen contentComponent, as provided internally
DrawerItems
; - 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:
- Navigate: an aid method to navigate to another screen;
- SetParams: change route parameter method;
- GoBack: Close the current screen and back;
- State: indicates the status or routing information of the current screen.
- Dispatch: Release action;
navigate
Navigate to another screen using the navigate method:
navigate(routeName, params, action)Copy the code
- RouteName: indicates the name of the destination route, which is the route key registered in the App navigation route.
- Params: parameters carried by the destination route.
- 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:
- Subscribe to the Redux Store and trigger store store operations when it changes;
- 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:
- Store: persistent store;
- Config: indicates the configuration object
- Storage: a persistence engine, such as LocalStorage and AsyncStorage;
- Transforms: Transforms called during the Rehydration and storage phases;
- Blacklist: specifies the key of the reducers that can be persistently ignored.
- 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:
- Uniform Immutable for the entire state tree of redux Store
- Redux persistence compatibility with Immutable data;
- 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:
- Can initiate XMLHttpRequest in the browser, can also initiate HTTP requests in node.js side;
- Supporting Promise;
- Intercepting requests and responses;
- Can cancel the request;
- 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.
- Login successful. Distribute
LoginActions.loginSuccess
Action, and then the Reducer and listen on this actionloginSuccessSaga
saga; - Logon failed. Distribution
LoginActions.loginFail
action;
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
- react native
- React Native Chinese
- react navigation