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:
- Application-wide
state
Are 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. - State is read-onlyThe only way to change state is to trigger
action
.action
Is a generic object that describes events that have occurred. - 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 – storyProvider
A component injects a store into an application through context, which the component then usesconnect
The 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:
- Components through
connect()(Comp)
Connect the story. - 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:
- create
history
Object, which is used because our application is browser-sidecreateBrowserHistory
create - use
connectRouter
The package root Reducer also provides the ones we createdhistory
Object to obtain a new root Reducer - use
routerMiddleware(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:
- use
ConnectedRouter
Package routing, and will be created in storehistory
Object is introduced, passed into the application as props ConnectedRouter
Component to act asProvider
The 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:
fetchPosts
Returns a function, whereas normal Action Creator returns an object by default.- The argument to the function returned is
dispatch
andgetState
For both Redux methods, the normal Action Creator argument is the content of the Action. - Of the returned functions, emit one
store.dispatch({type: SET_DEMO_DATA.PENDING})
, indicating that the asynchronous operation starts. - After the asynchronous operation is complete, issue another
store.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