In the last article, we introduced Webpack to automatically build React applications. Our local development server can support us to write React applications well, and support hot update code. This section begins a detailed analysis of how to build a React application architecture.

See github for the full project code

Personal blog

preface

There are many scaffolding tools available, such as create-React-app, that allow you to create a React application project structure with one click. However, you lose the opportunity to fully learn the project architecture and technology stack, and often the application architecture created by scaffolding 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

We are not going to use any scaffolding for this practice, so we need to create each file ourselves, introduce each technology and tripartite library, and eventually form the complete application, including the complete technology stack we chose.

The first step, of course, is to create a directory, which we did in the last post. If you don’t have the code, you can get it from Github:

git clone https://github.com/codingplayboy/react-blog.git
cd react-blog
Copy the code

The generated project structure is shown as follows:

  1. srcIs the application source code directory;
  2. webpackConfigure the directory for Webpack;
  3. webpack.config.jsConfigure the entry file for Webpack;
  4. package.jsonFor project dependency management files;
  5. yarn.lockLock files for project-dependent versions;
  6. .babelrcConfigure files for Babel to compile React and JavaScript code with Babel
  7. eslintrcandeslintignoreChecks configuration for ESLint syntax and content or files that need to be ignored;
  8. postcss.config.jsConfiguration file for CSS post-compiler postCSS;
  9. API.mdIs the API document entry;
  10. docsIs the document directory;
  11. README.mdIs the project description document;

The following work is mainly to enrich the SRC directory, including building the project architecture, developing application functions, as well as automation, unit testing, etc. This paper mainly focuses on building the project architecture, and then using the technology stack practice to develop several modules.

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 and the React-DOM library are project premises;
  2. The react routing;
  3. Application state management container;
  4. Whether Immutable data is required;
  5. Persistence of application state;
  6. Asynchronous task management;
  7. Testing and auxiliary 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. The react to react – dom;
  2. React-router manages application routes.
  3. Redux acts as a JavaScript state container. React-redux connects react 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. Optionally use the Reactotron debugging tool

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

Develop debugging tools

React application development Currently, there are many debugging tools, such as Redux-devTools and Reactron.

redux-devtools

Redux-devtools is a redux development tool that supports hot reloading, playback action, and custom UI.

The Redux toolbar can be viewed in the browser console by following the corresponding browser plug-in and then adding the relevant configuration to the Redux application.

Then install the project dependencies library:

yarn add --dev redux-devtools
Copy the code

It is then passed to the createStore method as a Redux reinforcer when the Redux Store is created:

import { applyMiddleware, compose, createStore, combineReducers } from 'redux'
// Combinatorial functions provided by default for redux
let composeEnhancers = compose

if (__DEV__) {
  // In the development environment, enable redux-devTools
  const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  if (typeof composeWithDevToolsExtension === 'function') {
    // Support redux development tool extension composition functions
    composeEnhancers = composeWithDevToolsExtension
  }
}

// create store
conststore = createStore( combineReducers(...) , initialState,// Combine the redux intermediate price and intensifier to strengthen reduxcomposeEnhancers( applyMiddleware(... middleware), ... enhancers ) )Copy the code
  1. Get the extended combinatorial functions provided by Redux-DevTools in the development environment;
  2. Redux-dev-tools gets information about applying Redux by combining the redux middleware and enhancer with the extension composition function when creating a store;

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:

First install:

yarn add --dev reactotron-react-js
Copy the code

Then initialize the reactotron-related configuration:

import Reactotron from 'reactotron-react-js';

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: 'React Blog' })

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

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

  : App

Copy the code

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

components

React componentized development principle Is that 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

Redux

If there is no state management container for any large Web application at present, the application will lack the characteristics of The Times. The 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 suitable for React Native applications.

Redux is a predictable state management container for JavaScript applications. It doesn’t depend on a specific framework or class library, so it has a consistent development style and efficiency across multiple platforms. It also makes it easy to implement time travel, i.e. playback of actions.

  1. Single-source data principle: Use Redux as the application state management container to centrally manage the application state tree. It pushes the principle of single trusted source data. All data comes from Redux Store and all data updates are processed by Redux.
  2. Redux Store state tree: Redux centrally manages application state. It is organized as a tree, just like the DOM tree and React component tree.
  3. Redux and Store: Redux is a Flux implementation scheme, so a store was created, which is similar to a store, manages application state centrally, and supports distributing every published action to all reducer;
  4. Action: exists in object data format. It usually has at least type and payload attributes. It describes tasks defined in REdux.
  5. Reducer: usually exist in the form of functions, receive state (application local state) and action object, perform different tasks according to action.type(action type), follow the idea of functional programming;
  6. Dispatch: a functional method provided by the store to distribute an action, passing an action object parameter;
  7. CreateStore: create a store, receive reducer, initialize the application state, redux middleware and enhiller, initialize the store, and start listening for actions;

Redux Middleware

Redux middleware, like Node middleware, can do some extra work before actions are distributed to the reducer. The actions published by Dispatch will be successively passed to all middleware and finally arrived at reducer. Therefore, middleware can be used to expand such as logging, adding monitoring, Switch routes and so on, so middleware essentially just extends the store.dispatch approach.

Store Enhancer

There may be times when we want to enhance the Store rather than just extend the Dispatch method. Redux provides enhancements to all aspects of the Store in the form of enhancers, and can even completely customize all interfaces on a store object, not just the store.Dispatch method.

const logEnhancer = (createStore) = > (reducer, preloadedState, enhancer) => {
  const store = createStore(reducer, preloadedState, enhancer)
  const originalDispatch = store.dispatch
  store.dispatch = (action) = > {
    console.log(action)
    originalDispatch(action)
  }
  
  return store
}
Copy the code

In the simplest example, the new function receives redux’s createStore method and the parameters needed to create a store, saves a reference to a method on the store object inside the function, reimplements the method, and calls the original method after processing the enhanced logic inside to ensure that the original function executes properly. This enhances the Store’s dispatch method.

As you can see, the enhancer does exactly what the middleware does, and middleware does it in the form of an enhancer. Its compose method can be composed to extend the enhancer to the Store, and if we pass in middleware, we call the applyMiddleware method to wrap it up. Internally, middleware capabilities are extended to the Store.dispatch method in the form of enhancers

react-redux

Redux is a standalone JavaScript app state management container library that works with React, Angular, Ember, jQuery, and even native JavaScript apps. Only Redux can be used to manage application status in a unified manner. Use the official React-Redux library.

class App extends Component {

  render () {

    const { store } = this.props

    return (

      <Provider store={store}>

        <div>

          <Routes />

        </div>

      </Provider>

    )

  }

}

Copy the code

The React-Redux library provides the Provider component to inject a store into an application using context. Then, you can use the connect high-order method to obtain and listen on the store, calculate the new props according to the store state and the component’s own props, and inject the component. In addition, you can compare the calculated new props to determine whether the component needs to be updated by listening on the Store.

For more on React-Redux, read the previous article: React-Redux Analysis.

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

  // Middlewares

  // Build the middleware for intercepting and dispatching navigation actions

  const blogRouteMiddleware = routerMiddleware(history)

  const sagaMiddleware = createSagaMiddleware()

  const middleware = [blogRouteMiddleware, sagaMiddleware]



  // enhancers

  const enhancers = []

  let composeEnhancers = compose



  // create store

  const store = createStore(

    combineReducers({

      router: routerReducer,

. reducers

    }),

    initialState,

    composeEnhancers(

applyMiddleware(... middleware),

. enhancers

    )

  )

  sagaMiddleware.run(saga)



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

. RouterReducer,

. AppReducer

  });



  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.

In addition, each state tree node data is Immutable, such as AppReducer:

const initialState = Immutable.fromJS({

  ids: [],

  posts: {

    list: [],

    total: 0,

    totalPages: 0

  }

})



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

  case 'RECEIVE_POST_LIST':

    const newState = state.merge(action.payload)

    return newState || state

  default:

    return state

}

Copy the code

By default, the immutable.fromjs () method is used to convert state tree objects to Immutable structures, and the Immutable method state.merge() is used to update state to ensure uniform and predictable state.

The React routing

In the React Web single-page application, the display and switchover of page-level UI components are completely controlled by routes. Each route has a corresponding URL and route information. We can use routes to manage component switchover in a unified and efficient manner, keeping THE UI and URL in sync, ensuring stability and friendly application experience.

react-router

React Router is the most popular route management library for developing React applications. It provides a simple API for implementing powerful routing functions such as load on demand and dynamic routing in a declarative manner.

  1. Declarative: concise and clear syntax;
  2. Loading on demand: lazy loading, according to the use of the need to determine whether to load;
  3. Dynamic routing: dynamic combination application routing structure, more flexible, more in line with the componentized development mode;

Dynamic and static routes

With React-Router V4, you can define a cross-platform Dynamic Routing structure for applications. Dynamic Routing takes place during rendering and does not need to be configured before the application is created. This is why it is different from Static Routing. Dynamic Routing improves a more flexible route organization and is more convenient for coding to load components on demand.

In React – Router V2 and V3, developing react applications requires defining a complete application route structure before rendering. All routes need to be initialized at the same time to take effect after rendering. As a result, many nested routes are generated, which loses the flexibility of dynamic routes and the concise loading on demand coding method.

react-router v4.x

In the React-Router 2.x and 3.x versions, defining an application routing structure is usually as follows:

import React from 'react'

import ReactDOM from 'react-dom'

import { browserHistory, Router, Route, IndexRoute } from 'react-router'



import App from '.. /components/App'

import Home from '.. /components/Home'

import About from '.. /components/About'

import Features from '.. /components/Features'



ReactDOM.render(

  <Router history={browserHistory}>

    <Route path='/' component={App}>

      <IndexRoute component={Home} />

      <Route path='about' component={About} />

      <Route path='features' component={Features} />

    </Route>

  </Router>,

  document.getElementById('app')

)

Copy the code

Simple, but all routing structures need to be defined and nested in layers before rendering the application; And if you want to achieve asynchronous on-demand loading also need to modify the routing configuration object here, use getComponentAPI, and invade the transformation of the component, with webpack’s asynchronous packaging loading API, to achieve on-demand loading:

  1. Routing layers are nested and must be declared uniformly before rendering applications;
  2. The API is different and needs to be usedgetComponentTo increase the complexity of routing configuration objects.
  3. <Route>It is an auxiliary label that declares the route, and is meaningless.

React-router v4.x is used as follows:

// react-dom (what we'll use here)

import { BrowserRouter } from 'react-router-dom'



ReactDOM.render((

  <BrowserRouter>

    <App/>

  </BrowserRouter>

), el)



const App = () => (

  <div>

    <nav>

      <Link to="/about">Dashboard</Link>

    </nav>

    <Home />

    <div>

      <Route path="/about" component={About}/>

      <Route path="/features" component={Features}/>

    </div>

  </div>

)

Copy the code

Compared to the previous version, reduced the configuration of the trace, highlighting the modular organization, and the rendering component to implement the routing of this part, and if you expect demand to load this component, you can implement a support asynchronous loading component by encapsulating the high-order component, will return to components after dealing with the high order component incoming < Route >. Still following the componentized form:

  1. Flexibility: Routes can be declared in the rendering component, do not depend on other routes, and do not require centralized configuration;
  2. Brevity: uniform incomingcomponentTo ensure the simplicity of route declaration;
  3. Modular:<Route>Create routes as a real component that can be rendered;

Route hook method

It is also important to note that compared with the previous version, the API provides hook methods such as onEnter, onUpdate, onLeave, etc., which improves the controllability of routing to some extent, but essentially only overwrites the life cycle method of the rendering component. Now we can directly control routing through the life cycle method of the route rendering component. Use componentDidMount or componentWillMount instead of onEnter.

Routing and story

When react-Router and Redux are used together, this works in most cases. However, the route change component may not be updated, for example:

  1. We use Redux’sconnectMethod to connect a component to redux:connect(Home);
  2. Component is not a route rendering component, that is, is not usedRoute>Component form:<Route component={Home} />Declared rendered;

Why is that? Because Redux implements the component’s shouldComponentUpdate method, when the route changes, the component does not receive props indicating that it has changed and needs to update the component.

So how to solve the problem? To solve this problem, simply wrap the component with the withRouter method provided by react-router-dom:

import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Home))
Copy the code

Redux integration

After using Redux, you need to follow the Redux principle: Single trusted data source, that is, all data sources can only be reudx store. React routing state should be no exception, so you need to connect route state and Store state.

react-router-redux

Redux {react-router}; Redux {react-router}; Redux {react-router}; Redux {react-router};

yarn add react-router-redux@next
yarn add history
Copy the code

Then, when creating a Store, you need to implement the following configuration:

  1. Create a history object, for web applications, we choose browserHisotry, need from the history/createBrowserHistory introduced module createHistory method to create the history object;

    See more about History here

  2. Add routerReducer and routerMiddleware, where the routerMiddleware receives the History object and connects to store and History, which is the same as the old version of syncHistoryWithStore.

import createHistory from 'history/createBrowserHistory'
import { ConnectedRouter, routerReducer, routerMiddleware, push } from 'react-router-redux'
// Create a history of your choosing (we're using a browser history in this case)
export const history = createHistory()

// Build the middleware for intercepting and dispatching navigation actions
const middleware = routerMiddleware(history)

// Add the reducer to your store on the `router` key
// Also apply our middleware for navigating
conststore = createStore( combineReducers({ ... reducers,router: routerReducer
  }),
  applyMiddleware(middleware)
)

return store
Copy the code

When rendering the root component, we abstracted two components:

  1. Initialize render root component, mount to DOM root component, from<Provider>Component wrap, injected into store;
  2. Route configuration component, in the root component, declare the route configuration component, initialize the necessary application route definition and route object;
import createStore from './store/'
import Routes from './routes/'
import appReducer from './store/appRedux'

const store = createStore({}, {
  app: appReducer
})

/** * Project root * @class App * @extends Component */
class App extends Component {
  render () {
    const { store } = this.props

    return (
      <Provider store={store}>
        <div>
          <Routes />
        </div>
      </Provider>)}}// Render the root component
ReactDOM.render(
  <App store={store} />,
  document.getElementById('app')
)
Copy the code

The

component above is the routing component of the project:

import { history } from '.. /store/'
import { ConnectedRouter } from 'react-router-redux'
import { Route } from 'react-router'

class Routes extends Component {
  render () {
    return (
      <ConnectedRouter history={history}>
        <div>
          <BlogHeader />
          <div>
            <Route exact path='/' component={Home} />
            <Route exact path='/posts/:id' component={Article} />
          </div>
        </div>
      </ConnectedRouter>
    )
  }
}
Copy the code

The ConnectedRouter component will automatically use the store injected by the component. What we need to do is manually pass in the history property. The history.listen method is called inside the component to listen for the browser LOCATION_CHANGE event, and finally the < router > component of the react-router is returned to handle the route configuration passed in as this.props. Children. ConnectedRouter Component content transfer.

Dispatch Route Switchover

After configuring the code above, route switches and component updates can be triggered as a Dispatch action:

import { push } from 'react-router-redux'

// Now you can dispatch navigation actions from anywhere!

store.dispatch(push('/about'))

Copy the code

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

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, we can use localStorage to store some data, if it is a large amount of data storage can use webSQL.

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.

yarn add redux-persist
Copy the code

The persistent store

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

// configure persistStore and check reducer version number

if (ReduxPersistConfig.active) {

  RehydrationServices.updateReducers(store);

}

Copy the code

This method implements persistent store:

// Check to ensure latest reducer version

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

if (localVersion ! == reducerVersion) {

/ / to empty the store

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

    storage.setItem('reducerVersion', reducerVersion);

  } else {

    persistStore(store, null, startApp);

  }

}).catch(() => {

  persistStore(store, null, startApp);

  storage.setItem('reducerVersion', reducerVersion);

})

Copy the code

A Reducer version number will be stored in the localStorage, which can be configured in the application configuration file. When the reducer version number is first implemented, this version number and store will be stored. If the reducer version number changes, the original store will be cleared. 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 localStorage, 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 released to V5. x, and v5.x is used as an example in this article. V5. x is referenced here.

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. React routes are compatible with Immutable.

Check out Immutable and React,Redux and Reselect practices in our previous article on Immutable.

Immutable and React routes

How to connect react routing states to the Redux store is a simple way to connect router states to the Redux store. RouteReducer cannot handle Immutable. We need to create a new RouterReducer that controls Immutable.

import Immutable from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';

const initialState = Immutable.fromJS({
  location: null
});

export default (state = initialState, action) => {
  if (action.type === LOCATION_CHANGE) {
    return state.set('location', action.payload);
  }
  
  return state;
};
Copy the code

Convert the default initial route state to Immutable, and use the Immutable API to manipulate state when changing routes.

seamless-Immutable

With the introduction of Immutable. Js, the API for using application state data structures must follow the Immutable API, instead of using native JavaScript objects, arrays, etc., such as array destructing ([a, b] = [b, c]), object extenders (…). Etc., there are some problems:

  1. Immutable Data a large number of secondary nodes:
  2. The Immutable syntax must be used, which is different from JavaScript syntax and is not compatible.
  3. When writing with JavaScript libraries such as Redux and React-Router, you need to introduce additional compatible processing libraries;

To address these issues, the community has a seamless-immutable alternative:

  1. Lighter: relative to Immutable. Jsseamless-immutableThe library is lighter and smaller;
  2. Syntax: Object and array manipulation syntax is closer to native JavaScript;
  3. Easier to collaborate with other JavaScript libraries;

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 { HomeSaga } from './Home/flux.js'

import { AppSaga } from './Appflux.js'



const sagas = [

. AppSaga,

. HomeSaga

]



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

In the case of AppSaga, we expect to make some asynchronous requests when the application starts, such as getting the article list data to populate the Redux Store, without waiting for the components that use the data to finish rendering, to improve the response time:

const REQUEST_POST_LIST = 'REQUEST_POST_LIST'

const RECEIVE_POST_LIST = 'RECEIVE_POST_LIST'



/ * *

* Request article list ActionCreator

 * @param {object} payload

* /

function requestPostList (payload) {

  return {

    type: REQUEST_POST_LIST,

    payload: payload

  }

}



/ * *

* Receive article list ActionCreator

 * @param {*} payload

* /

function receivePostList (payload) {

  return {

    type: RECEIVE_POST_LIST,

    payload: payload

  }

}



/ * *

* Handle requested article list Saga

* @param {*} payload Payload of a request parameter

* /

function * getPostListSaga ({ payload }) {

  const data = yield call(getPostList)

  yield put(receivePostList(data))

}



/ / define AppSaga

export function * AppSaga (action) {

// Receive the last request, then call getPostListSaga subsaga

  yield takeLatest(REQUEST_POST_LIST, getPostListSaga)

}

Copy the code
  1. takeLatestIn:AppSagausetakeLatestMethods to monitorREQUEST_POST_LISTAction. If multiple actions are initiated consecutively within a short period of time, the previous unresponded action will be cancelled and only the last action will be initiated.
  2. getPostListSagaSubsaga: called when the action is receivedgetPostListSagaAnd pass it the payload,getPostListSagaIs a child of AppSaga, which handles specific asynchronous tasks;
  3. getPostList:getPostListSagaWill be calledgetPostListMethod, make an asynchronous request, get the response data, callreceivePostListActionCreator, create and distribute actions, and the corresponding logic is processed by reducer;

The getPostList method reads as follows:

/ * *

* Request the article list method

* @param {*} Payload Request parameter

 *  eg: {

 *    page: Num,

 *    per_page: Num

*}

* /

function getPostList (payload) {

  return fetch({

. API.getPostList,

    data: payload

  }).then(res => {

    if (res) {

      let data = formatPostListData(res.data)

      return {

        total: parseInt(res.headers['X-WP-Total'.toLowerCase()], 10),

        totalPages: parseInt(res.headers['X-WP-TotalPages'.toLowerCase()], 10),

. data

      }

    }

  })

}

Copy the code

Put is the redux-Saga distributable action method. Take, call, and so on are the REdux-Saga APIS.

You can then inject ActionCreator into the project routing root component, create the action, and saga will receive it for processing.

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, Redux application and project engineering practice, so as to continue to forge ahead in the road of big front-end growth.

React Router, Redux, react-redux, react-router-redux, redux-Saga, axios. Immutable, Reactotron, Redux Persist.

See github for the full project code

reference

  1. React
  2. Redux
  3. React Router v4
  4. redux-saga
  5. Redux Persist