The last article — Building a React application from Scratch — described how to build a very basic React development environment using WebPack. This article details how to build a React application architecture.

Warehouse address: github.com/MrZhang123/…

redux

In our development process, a lot of times, we need to make components share some data, although can transfer data through component to realize data sharing, but if not a parent-child relationship between components, data transmission is very troublesome, and easy to let the readability of the code is reduced, this time we need a state (state) management tool. Common state management tools include Redux and MOBx. Here, redux is used for state management. Note that React 16.3 brings a new Context API, which we can also use for state management.

Redux is a JavaScript state container that provides predictable state management. Allows you to build consistent applications that run in different environments (client, server, native application) and are easy to test. Not only that, it also provides a great development experience, such as a time travel debugger that can be edited in real time.

The data flow for Redux is shown below:

Redux’s three principles:

  1. Application-widestateAre stored in an object tree that exists only in a single store, but this does not mean that using Redux requires all state to be stored in Redux.
  2. State is read-onlyThe only way to change state is to triggeraction.actionIs a generic object that describes events that have occurred.
  3. Using pure functions to perform the modifications, you need to write reducers to describe how the action changes the State Tree.

Redux Middleware

Redux Middleware provides extension points after an action is initiated and before the reducer arrives. Actions initiated by Dispatch pass through the middleware and finally reach the reducer. Redux Middleware can be used for logging, creating crash reports, calling asynchronous interfaces, routing, and more. Essentially, middleware just extends the store.dispatch approach.

Store enhancer

Store enhancer is used to enhance store functionality. A store enhancer is actually a higher-order function that returns a new enhanced Store creator.

const logEnhancer = createStore= > (reducer, initialState, enhancer) => {
  const store = createStore(reducer, initialState, enhancer)
  function dispatch(action) {
    console.log(`dispatch an action: The ${JSON.stringify(action)}`)
    const res = store.dispatch(action)
    const newState = store.getState()
    console.log(`current state: The ${JSON.stringify(newState)}`)
    return res
  }
  return { ...store, dispatch }
}
Copy the code

You can see that the logEnhancer has changed the default behavior of the Store to print logs before and after each dispatch.

react-redux

Redux is a state-js state library that can be used with React, Vue, Angular, and even native JS applications. To manage react state, connect Redux to React. The React-Redux library is officially available.

The react – storyProviderA component injects a store into an application through context, which the component then usesconnectThe higher-order method obtains and listens to the Store, calculates the new props according to the store state and the props of the component, and injects the component. In addition, it can compare the calculated new props to determine whether the component needs to be updated by listening to the Store.

render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>.document.getElementById('app'))Copy the code

Integrate Redux into react

Merger of reducer

In a React application, there is only one store. The component calls the action function and sends data to the Reducer. The Reducer changes the corresponding state based on the data. However, as the application complexity increases, the Reducer will also become larger and larger. At this time, we can consider splitting the Reducer into several separate functions, and each function after splitting is responsible for independently managing part of the state.

Redux provides combineReducers helper functions that combine the fragmented reducer into a final reducer function, which is then used when createStore is created.

Integration middleware

Sometimes we need multiple middleware chains to enhance store.dispatch, and when we create a store, we need to integrate the middleware chains into the store. ApplyMiddleware (… Middleware) chains middleware together.

Integrated store enhancer

Store enhancer used to enhance the store, if we have more than one store enhancer needs to integrate multiple store enhancer, this time will use the compose (… Functions provides).

Use compose to combine multiple functions. Each function takes one argument, and its return value is supplied as one argument to the function on its left and so on. The right-most function can take multiple arguments. Compose (funA,funB,funC) can be understood as compose(funA(funB(funC())))), which ultimately returns the final function of the combination of the functions received from right to left.

Create the Store

Redux creates a Redux store to store all the states in the app by createStore. The createStore parameters are as follows:

createStore(reducer, [preloadedState], enhancer)
Copy the code

So we create the store code as follows:

import thunk from 'redux-thunk'
import { createStore, applyMiddleware } from 'redux'

import reducers from '.. /reducers'

const initialState = {}

const store = createStore(reducers, initialState, applyMiddleware(thunk))

export default store
Copy the code

Then inject the created Store into the React application using the Provider component to integrate the Redux and react application.

Note: There should be only one store in the app.

React Router

The React Router is the complete React routing solution that keeps the UI and URL in sync. In this project, we integrated the latest version of React Router V4.

In react-router V4, react-router is divided into three packages: react-router, react-router-dom, and react-router-native.

  • React-router: Provides core routing components and functions
  • React-router-dom: the react router for browsers
  • React -router-native: Indicates the React router used by React Native

Story and react to the router

The React Router works fine for the most part when used with Redux, but occasionally routes are updated but child routes or active navigation links are not. This happens when:

  1. Components throughconnect()(Comp)Connect the story.
  2. A component is not a “routed component,” that is, the component does not look like<Route component={SomeConnectedThing} />Render like this.

The reason for this problem is that Redux implements shouldComponentUpdate, which does not receive the props update when the route changes.

The solution to this problem is simple, find Connect and wrap it in withRouter:

// before
export default connect(mapStateToProps)(Something)

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

Deep integration of Redux with React-Router

Sometimes we might want to integrate redux with the React Router in a deeper way:

  • Synchronize the router’s data with and access it from the Store
  • Navigate through Dispatch Actions
  • Time travel debugging for route changes is supported in Redux DevTools

This can be done by deep integration of react-Router with Redux via connected- React-Router and history libraries.

React-router-redux is referred to in official documentation and has been integrated into React-Router V4. However, according to the react-router-Redux documentation, this repository is no longer maintained. Connected – React-router is recommended.

Install connected-react-router and history libraries:

$ npm install --save connected-react-router
$ npm install --save history
Copy the code

Then add the following configuration to store:

  • createhistoryObject, which is used because our application is browser-sidecreateBrowserHistorycreate
  • useconnectRouterThe package root Reducer also provides the ones we createdhistoryObject to obtain a new root Reducer
  • userouterMiddleware(history)The implementation uses dispatch History Actions so that it can be usedpush('/path/to/somewhere')To change the route (push from connected- React-router)
import thunk from 'redux-thunk'
import { createBrowserHistory } from 'history'

import { createStore, applyMiddleware } from 'redux'
import { connectRouter, routerMiddleware } from 'connected-react-router'

import reducers from '.. /reducers'

export const history = createBrowserHistory()
const initialState = {}

const store = createStore(
  connectRouter(history)(reducers),
  initialState,
  applyMiddleware(thunk, routerMiddleware(history))
)

export default store
Copy the code

In the root component, we add the following configuration:

  • useConnectedRouterPackage routing, and will be created in storehistoryObject is introduced, passed into the application as props
  • ConnectedRouterComponent to act asProviderThe child components
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'

import App from './App'
import store from './redux/store'
import { history } from './redux/store'

render(
  <Provider store={store}>
    <ConnectedRouter history={history}>
      <App />
    </ConnectedRouter>
  </Provider>.document.getElementById('app'))Copy the code

This completes the redux integration with the React-Router.

Switch routes using Dispatch

After the above configuration is complete, you can use Dispatch to switch routes:

import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
Copy the code

react-router-config

React-router before V4 — static routes

In versions prior to React-Router V4, we could configure application routing directly using static routing, which allowed routing to be checked and matched prior to rendering.

Router.js typically has code like this:

const routes = (
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User} />
      </Route>
      <Route path="*" component={NoMatch} />
    </Route>
  </Router>
)
export default routes
Copy the code

Then import the route at initialization and render it:

import ReactDOM from 'react-dom'
import routes from './config/routes'

ReactDOM.render(routes, document.getElementById('app'))
Copy the code

React-router V4 — Dynamic routing

As of version v4, the React router uses dynamic components instead of path configuration. The React router is a normal component of the React application. So the React application adds the React-Router to introduce what we need first.

import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'
Copy the code

Here we introduce BrowserRouter and rename it Router. BrowserRouter allows the React-Router to pass application routing information through the context to any component that needs it. So for the React-Router to work, you need to render BrowserRouter at the root of your application.

import React from 'react'
import { BrowserRouter as Router, Route, Link } from 'react-router-dom'

class App extends Component {
  render() {
    return (
      <Router>
        <div>
          <div>
            <Link to="/">Home</Link>
          </div>
          <hr />
          <Route exact path="/" component={Home} />
        </div>
      </Router>)}}Copy the code

Route is also used, which renders the specified Component when the application’s Location matches a Route, and null otherwise.

To add more routes, add the Route component, but this may feel a bit confusing because routes are scattered across components, making it difficult to see the entire application Route as easily as before, and if the project used react-Router PRIOR to V4, The react-router-config library is designed to handle static route configuration.

Add react-router-config to use static routes

After adding react-router-config, we can write familiar static routes. At the same time, it can be used to spread the routing configuration across components, and finally use renderRoutes to merge the scattered routing fragments in the root component for rendering.

Configuring static routes:

import Home from './views/Home'
import About from './views/About'

const routes = [
  {
    path: '/'.exact: true.component: Home
  },
  {
    path: '/about'.component: About
  }
]
export default routes
Copy the code

Then merge in the root component and render:

import { renderRoutes } from 'react-router-config'

import HomeRoute from './views/Home/router'
import AboutRoute from './views/About/router'
// Merge routes
const routes = [...HomeRoute, ...AboutRoute]

class App extends Component {
  render() {
    return (
      <Router>
        <div className="screen">{renderRoutes(routes)}</div>
      </Router>)}}Copy the code

RenderRoutes actually does something similar for us:

const routes = (
  <Router>
    <Route path="/" component={App}>
      <Route path="about" component={About} />
      <Route path="users" component={Users}>
        <Route path="/user/:userId" component={User} />
      </Route>
      <Route path="*" component={NoMatch} />
    </Route>
  </Router>
)
Copy the code

This adds static routes to the React application.

Hot Module Replacement

Module hot replacement (HMR) replaces, adds, or removes modules while the application is running without reloading the entire page. Mainly through the following ways:

  • Preserves application state that is lost when the page is fully reloaded
  • Only update what is changed to save development time
  • Changing styles does not require a page refresh

In development mode, HMR can replace LiveReload, and webpack-dev-server supports hot mode, which tries to update with HMR before attempting to reload the entire page.

Enable the HMR

Add the HMR plug-in to the WebPack configuration file:

plugins: [new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin()]
Copy the code

The NamedModulesPlugin added here,

Set webpack-dev-server to hot mode:

const server = new WebpackDevServer(compiler, {
+  hot: true.// noInfo: true,
  quiet: true.historyApiFallback: true.filename: config.output.filename,
  publicPath: config.output.publicPath,
  stats: {
    colors: true}});Copy the code

In this way, when the React code is modified, the page will be refreshed automatically, and the CSS file will be modified.

However, there is a problem that the react component’s state will be lost due to the automatic page refresh. Can you change the React component like changing the CSS file without refreshing the page? The answer is yes, use the React-hot-loader.

Add the react – hot – loader

Adding a react-hot-loader is very simple. You just need to add the high-order hot method when the root component is exported:

import { hot } from "react-hot-loader";

class App extends Component {... }export default hot(module)(App);
Copy the code

This way, the entire application can be modified while developing the React component and remain in state.

Note:

During development, I looked up some articles that said that in order to work with Redux, I needed to add the following code to store.js:

if (process.env.NODE_ENV === 'development') {
  if (module.hot) {
    module.hot.accept('.. /reducers/index.js', () = > {// const nextReducer = combineReducers(require('.. /reducers'))
      // store.replaceReducer(nextReducer)
      store.replaceReducer(require('.. /reducers/index.js').default)
    })
  }
}
Copy the code

In react-hot-loader V4, however, this is not required. You can simply add hot.

Asynchronously loading components (Code Splitting)

After completing the above configuration, our main body was almost set up. However, when we opened the developer tool, we found that the JS of the entire application was directly loaded when the application started to load, but we expected to load the Code of the page in which we entered. So how to achieve Code Splitting of the application?

There are many libraries that implement React Code Splitting, for example:

  • Loadable Components
  • Imported Component
  • React Universal Component
  • React-Loadable

Just choose one of them. My project uses React-loadable.

We have configured static routing in the project before, the components are directly imported, we only need to process the directly imported components before, the code is as follows:

import loadable from 'react-loadable'
import Loading from '.. /.. /components/Loading'

export const Home = loadable({
  loader: (a)= > import('./Home'),
  loading: Loading
})
export const About = loadable({
  loader: (a)= > import('./About'),
  loading: Loading
})

const routes = [
  {
    path: '/'.exact: true.component: Home
  },
  {
    path: '/about'.component: About
  }
]
export default routes
Copy the code

Asynchronous task flow management

The idea of realizing asynchronous operation

Most of the time we have synchronous operations in our application, i.e. with a Dispatch action, the state is updated immediately, but sometimes we need to do asynchronous operations. Synchronous operations issue only one Action, but asynchronous operations need to issue three ACions.

  • Action when the Action is initiated
  • Action if the operation succeeds
  • Action when an operation fails

To distinguish between the three actions, you might add a special status field to the action as a flag bit:

{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS'.status: 'error'.error: 'Oops' }
{ type: 'FETCH_POSTS'.status: 'success'.response: {... }}Copy the code

Or define different types for them:

{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE'.error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS'.response: {... }}Copy the code

So to implement asynchronous operations you need to:

  • When the Action begins, issue an Action that triggers the State update to “in Action” and the View re-renders
  • When the Action is complete, issue another Action that triggers an update of State to “operation complete” and the View re-renders again

redux-thunk

An asynchronous operation sends out at least two actions. The first Action can be sent directly, just like a synchronous operation.

We can send an Action Creator function along with the first Action, so that the second Action can be sent automatically after the asynchronous execution is complete.

componentDidMount() {
   store.dispatch(fetchPosts())
}
Copy the code

After the component loads successfully, an Action is sent to request data, in this case fetchPosts is Action Creator. FetchPosts looks like this:

export const SET_DEMO_DATA = createActionSet('SET_DEMO_DATA')

export const fetchPosts = (a)= > async (dispatch, getState) => {
  store.dispatch({ type: SET_DEMO_DATA.PENDING })
  await axios
    .get('https://jsonplaceholder.typicode.com/users')
    .then(response= > store.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response }))
    .catch(err= > store.dispatch({ type: SET_DEMO_DATA.ERROR, payload: err }))
}
Copy the code

FetchPosts is an Action Creator that returns a function that dispatches an Action to indicate that an asynchronous operation is imminent; After the asynchronous execution is complete, different actions are dispatched to return the results of the asynchronous operation according to the different request results.

A few points to note here:

  1. fetchPostsReturns a function, whereas normal Action Creator returns an object by default.
  2. The argument to the function returned isdispatchandgetStateFor both Redux methods, the normal Action Creator argument is the content of the Action.
  3. Of the returned functions, emit onestore.dispatch({type: SET_DEMO_DATA.PENDING}), indicating that the asynchronous operation starts.
  4. After the asynchronous operation is complete, issue anotherstore.dispatch({ type: SET_DEMO_DATA.SUCCESS, payload: response })Is displayed, indicating that the operation is complete.

But there is a problem. Normally, store.dispatch can only send objects, but we want to send functions. To enable store.dispatch to send functions, we use middleware — redux-thunk.

Introducing Redux-thunk is as simple as using applyMiddleware(thunk) at store creation time.

Develop debugging tools

Debugging is inevitable during the development process. There are many debugging tools commonly used, such as Redux-DevTools-Extension, Redux-DevTools, storybook, etc.

redux-devtools-extension

Redux-devtools-extension is a handy tool for debugging Redux and monitoring actions.

Download the plug-in from the Chrome Web Store or Mozilla Add-ons, depending on your browser.

Then add it to the Store enhancer configuration when creating a store:

import thunk from "redux-thunk";
import { createBrowserHistory } from "history";

import { createStore, applyMiddleware } from "redux";
+ import { composeWithDevTools } from "redux-devtools-extension/logOnlyInProduction";
import { connectRouter, routerMiddleware } from "connected-react-router";

import reducers from ".. /reducers";

export const history = createBrowserHistory();
const initialState = {};

+  const composeEnhancers = composeWithDevTools({
+   // options like actionSanitizer, stateSanitizer+});const store = createStore(
  connectRouter(history)(reducers),
  initialState,
+  composeEnhancers(applyMiddleware(thunk, routerMiddleware(history)))
);
Copy the code

Write in the last

This paper summarizes my knowledge of React application architecture and the specific configuration of related libraries to further deepen my understanding of React application architecture. However, Immutable data, persistence, webpack optimization and other related things are not covered in this paper, and will continue to be studied in the future. Try to build a better React application.

In addition, @babel/preset-stage-0 is about to be deprecated after updating the latest Babel during the build project, so it is recommended to use other presets instead. For more details, please refer to:

  • Cannot read property ‘join’ of undefined ( preset-stage-0 )
  • About @ Babel/preset – stage – 0

The attached

Key words:

  • redux
  • react-router
  • react-router-config
  • Asynchronous loading (Code Splitting)
  • Hot update
  • Asynchronous task management — redux-thunk
  • react-redux
  • redux-devtools-extension

Part of the library used

  • connected-react-router
  • history
  • react-hot-loader
  • react-router-config

reference

  • React Application architecture design
  • Brief analysis of Redux’s Store Enhancer
  • createStore
  • applyMiddleware
  • combineReducers
  • compose
  • React Router V4
  • The React Router integrates with Redux
  • Hot Module replacement
  • React-router4 route splitting and on-demand loading based on react-router-config
  • React Router 4 Introduces React Router 4 and the routing philosophy behind it
  • Asynchronous Action
  • Redux-thunk for Redux middleware
  • Redux Tutorial ii: Middleware and Asynchronous Operations