Github.com/hql123/reac…


Project introduction

Project constantly update perfect, at present the functions, was written while writing code documentation, everyone structures, the React project when the habit is different, I just want to share my own experience in learning the React, if you feel my project of beginners helpful to you, can give a start please? Pat ~

Please read the Redux Chinese documentation before using it


Step 1: Start the project and initialize it

Global installationcreate-react-app

npm install -g create-react-app

Initialize the project (ruby-China is the folder name)

After create-react-app ruby-China is installed successfully, the following contents will be displayed





Paste_Image.png

Go to the directory CD ruby-china. Enter ls. The directory contains the following files





Paste_Image.png

Node_modules is a third-party installation package, which is ignored by default in. Gitignore, package.json is a third-party installation configuration file, public stores static HTML or images, etc. SRC is the application directory, js or JSX files, CSS files, package files, etc. This is the default directory structure to start with create-react-app, but you can also customize it.

Now that you have a simple “Welcome to React” project, let’s get started. NPM init ($git, $git, $git, $git, $git, $git) NPM install –save react react-dom

Advice over the wall, there is no advice over the wall to modify NPM mirror, such as: NPM config set registry https://registry.npm.taobao.orgnpm above address may have to modify, give priority to with the latest version of the image

NPM start: localhost:3000 by default, port localhost:3000 is automatically opened





Paste_Image.png

If you want to customize a webpack or gulp build package project, you can customize startup commands in package.json, such as:

"scripts": {
    "start": "node server.js",
    "build": "node build.js",
    "test": "react-scripts test --env=jsdom",
    "eject": "react-scripts eject"
  }Copy the code

I can also use gulp plus Webpack to create a separate flow of tasks. I’ve tried, but webpack is generally enough so far. In the development process, if the efficiency of Webpack is low and you have to use GULp to solve the problem, you probably need to use it together. The defects OF Webpack I found so far are as follows:

  1. [bug Mc-10879] – index.html is excluded when listening for file changes, so we need to refresh it manually

Since we use create-React-app to initialize the project, the project itself already contains all the third parties under React-Script, so we do not need to install other third-party packages of WebPack (:зゝ∠).

Uncaught SyntaxError: Unexpected token import Uncaught SyntaxError: Unexpected token import Uncaught SyntaxError: Unexpected token import

npm install --save-dev babel-cli babel-preset-es2015 babel-preset-react
npm install --save-dev babel-eslint eslint eslint-loader eslint-plugin-react eslint-config-react-app
npm install --save-dev babel-loader style-loader less less-loader file-loader url-loader css-loaderCopy the code

Create the.babelrc file touch. Babelrc and add the following code:

{
  "presets": ["es2015", "react"]
}Copy the code

Create the.eslintrc file touch.eslintrc and add the following code:

{
  "extends": "react-app"
}Copy the code

So let’s start configuring WebPack by creating a new config folder to store the configuration files:

The mkdir config touch config/webpack. Config. Dev. Js / / development environment configuration touch config/webpack config. Prod. Js/touch/production environment configuration Config /paths.js // File path config touch server.js // start fileCopy the code

Webpack. Config. Prod. Js and server. Js, I refer to react – scripts configuration file to make some small changes ruby – China/config/webpack. Config. Dev. Js

/ /.. /config/webpack.config.dev.js module.exports = { entry: [the require. Resolve (' react - dev - utils/webpackHotDevClient '), / / remove will not be able to monitor real-time file changes and refresh paths. AppIndexJs], the output: {path: path.join(__dirname, 'build'), pathinfo: true, filename: 'static/js/bundle.js', publicPath: publicPath }, devtool: 'cheap-module-source-map', plugins: [ new InterpolateHtmlPlugin({ PUBLIC_URL: publicUrl }), // Generates an `index.html` file with the <script> injected. new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, }), new webpack.DefinePlugin({ 'process.env.NODE_ENV': JSON.stringify( process.env.NODE_ENV || 'development' ) }), new webpack.HotModuleReplacementPlugin(), new CaseSensitivePathsPlugin(), new WatchMissingNodeModulesPlugin(paths.appNodeModules) ], ... module: { preLoaders: [ { test: /\.(js|jsx)$/, loader: 'eslint', include: paths.appSrc, } ], loaders: [{ test: /\.(js|jsx)$/, include: paths.appSrc, loader: 'babel', query: { babelrc: false, presets: [require.resolve('babel-preset-react-app')], cacheDirectory: true } }, { test: /\.(jpg|png|svg)$/, loader: 'file' query: {name: 'static/media / [name] [8] hash: [ext]'}}... / / code slightly... }}Copy the code

ruby-china/config/webpack.config.prod.js

/ /.. /config/webpack.config.prod.js ... // if (process.env.node_env! == "production") { throw new Error('Production builds must have NODE_ENV=production.'); }...Copy the code

Ruby/China /build.js

At this point, we have configured the basic Web static services, hot-load auto-refresh, and packaging of the production environment.

Start the service (don’t forget to set port number (:зゝ∠) in paths.js, default is 8890) NPM run start





Paste_Image.png

Packaging in production: NPM Run Build

Step 2: Add react-route+redux

Let’s install the libraries we’ll need after:

npm install react-router --save redux 
npm install isomorphic-fetch moment redux-logger react-redux react-router-redux redux-thunk --save-devCopy the code

Moment.js can easily manage time and date, as in moment.js example.

Use react-CSS-modules to generate a unique name for each CSS class when loading the CSS document. npm install –save-dev react-css-modules

1. redux

First, we will create the following directory:





Paste_Image.png

Here is an example of a normal View layer:

import {connect} from 'react-redux'; import {bindActionCreators} from 'redux'; var {addToSchedule} = require('.. /.. /actions'); class Sessions extends Component{ constructor(props) { super(props); } render(){// code}} function select(props) {return {isLoggedIn: store.user.isloggedin,}; } function actions(dispatch, props) { let id = props.session.id; return { addToSchedule: () => dispatch(addToSchedule(id)), } module.exports = connect(select, actions)(Sessions);Copy the code

1. Action

Create a new SRC /actions/index.js to combine each action:

/ /.. /src/actions/index.js 'use strict'; // const loginActions = require('./login'); // const scheduleActions = require('./schedule'); // const filterActions = require('./filter'); // const notificationActions = require('./notifications'); // const configActions = require('./config'); module.exports = { //... loginActions, // ... scheduleActions, // ... filterActions, // ... notificationActions, // ... configActions, };Copy the code

Action is the carrier for transferring data from the app to the Store. It is the only source of store data.

Function skipLogin(): action {return {type: 'SKIPPED_LOGIN',}; }... Function logIn(): ThunkAction {return (dispatch) => {// TODO: Make sure reducers clear their state return dispatch({ type: 'LOGGED_IN', data: [] }); }; }Copy the code

We can also write a configuration file called action.type to initialize the data structure that returns the data, such as:

/ /.. src/config/types.js 'use strict'; export type Action = { type: 'LOGGED_IN', data: { id: string; name: string; } } | { type: 'SKIPPED_LOGIN' } | { type: 'LOGGED_OUT' } ;Copy the code

It’s a good habit to think of the object’s data structure in advance (:з FLYING), although I’m pretty lazy every time.

It is important to note that when processing data asynchronously, it is best to separate request data, receive data, refresh data, and display data into separate actions to reduce coupling of request data to specific UI events.

Connect helper example:

function mapStateToProps(state, props) {
  return {
    isLoggedIn: store.user.isLoggedIn,
  };
}

function mapDispatchToProps(dispatch, props) {
  let id = props.session.id;
  return {
    addToSchedule: () => dispatch(addToSchedule(id)),
}

module.exports = connect(mapStateToProps, mapDispatchToProps)(Sessions);Copy the code

The only two arguments that connect() receives are select and Actions. MapStateToProps is a function that binds store data as props to the component, and store is passed as an argument to this function method. The mapDispatchToProps is a method in the Action serialized by Dispatch that is bound to the component as props. In mapStateToProps, the store can call the Dispatch () method directly through store.dispatch(), but in most cases we will accept the Dispatch argument directly using the mapDispatchToProps method. BindActionCreators () can automatically bind multiple action creator functions to the Dispatch () method.

function mapDispatchToProps(dispatch, props) {
let id = props.session.id;
  return bindActionCreators({
    addToSchedule: () => action.addToSchedule(id),
  });
}Copy the code
2. Reducer

After the Action has processed the data, it needs to be updated to the component. In this case, we need to Reducer the update state. First, we can create an initial state to determine what data needs to be manipulated by a Reducer branch in the Object tree:

//reducers/users.js
const initialState = {
  isLoggedIn: false,
  hasSkippedLogin: false,
  id: null,
  name: null,
};Copy the code

If we get the data object through a network request, we can specify the following fields in the initialized state: IsFetching progress of data, didInvalidate to indicate whether data is out of date, lastUpdated to store the last update of data, and data array to store fetching data, In practice, we’re going to need something like paging fetch pagecount and nextPageUrl.

Assume that the tabbed list data is obtained. You are advised to store the tabbed list data separately to ensure that the data can be updated immediately when users switch back and forth.

There is a clear division of work between Actions and Reducer. Keep the Reducer as clean as possible and never do the following operations in Reducer:

  • Modify the passed parameter;
  • Perform operations that have side effects, such as API requests and route jumps;
  • Call an impure function, such asDate.now()Math.random().

    The Redux documentation clearly indicates:

    As long as the arguments passed in are the same, the next state returned must be the same. No special cases, no side effects, no API requests, no variable changes, just perform calculations.

Chestnut:

//reducers/users.js function user(state = initialState, action) { switch (action.type) { case 'LOGGED_IN': return { ... state, isLoggedIn: true, hasSkippedLogin: false, action.data }; case 'SKIPPED_LOGIN': return { ... state, hasSkippedLogin: true, }; case 'LOGGED_OUT': return initialState; default: return state; }}Copy the code

Instead of using the documentation’s recommended object.assign () to create a new copy of state, I can avoid modifying state directly. I’m trying to reduce indentation and what looks like a complicated notation. See the ES7 object expansion operator for details.

Each Reducer has its own State managed exclusively. After decompression, use the combineReducers() utility class to integrate all reducers into the reducers/index.js file. For example, we then created an index.js file in reducers, which was used to combine multiple reducer files

/ /.. /src/reducers/index.js 'use strict'; var { combineReducers } = require('redux'); module.exports = combineReducers({ // sessions: require('./sessions'), // user: require('./user'), // topics: require('./topics'), });Copy the code

Of course you can also write:

/ /.. /reducers/index.js import { combineReducers } from 'redux' import * as reducers from './reducers' module.exports = combineReducers(reducers)Copy the code

The prerequisite is that each reducer function is exposed using export:

/ /.. /reducers/user.js ... module.exports = user; / / module.exports = {user1, user2,}; / / module.exports = {user1, user2,};Copy the code
3. Store

A Store is the object that links actions and reducers together. The Store has the following responsibilities:

Maintain application state; Provide the getState() method to getState; Provide a dispatch(action) method to update state; Register a listener with subscribe(listener); Unsubscribe the listener via a function returned by subscribe(listener). Again, Redux apps have a single store. When you need to split data processing logic, you should use a Reducer combination instead of creating multiple stores.

That’s the official explanation.

Here is an example to explain the Store configuration:

This step is to save the complete state tree returned by the root Reducer into a single Store.

/ /.. /store/configStore.js var reducers = require('.. /reducers'); import {createStore} from 'redux'; let store = createStore(reducers)Copy the code

Refactoring store

In order to make it easier for us to handle the following writing, we will make a small adjustment to the code directory structure, which is roughly as follows:

As you can see, we split configureStore.js into three files to respond to configurations in different environments:

if (process.env.NODE_ENV === 'production') { module.exports = require('./configureStore.prod') } else { module.exports =  require('./configureStore.dev') }Copy the code

Middlewares the state tree returned by reducer has been placed into the store. Middlewares are configured as follows:

const logger = createLogger(); const middlewares = [thunk, logger]; var createRubyChinaStore = applyMiddleware(... middlewares)(createStore);Copy the code

Middlewares includes the React-Thunk asynchronous load plugin and the React-Logger state tree tracker. After integration:

/ /.. / SRC/store/configureStore. Dev. Js / * * methods included in the const configureStore = (initialState) = > {} function in the body * /... const store = createStore( reducers, initialState, compose( applyMiddleware(... middlewares), DevTools.instrument() ) ) ...Copy the code

Let’s start configuring the Router.


2. The react – the router and the react – the router – story

What we want to do is a website, since it is a website must have a route. The above is nonsense. We need to understand why we use this routing configuration first, starting with the React-router: “It manages urls, allows component switching and state changes, and is almost certainly needed for complex applications.” The react-router-redux state manager is used as the react-router-redux state manager. The react-router-redux state manager is used as the react-router-redux state manager. React-router-redux: React-router-redux: React-router-redux: react-router-redux: react-router-redux: react-router-redux: react-router-redux: react-router-redux: react-router-redux

Configuration WebpackDevServer
// server.js
var historyApiFallback    = require('connect-history-api-fallback');
devServer: {
  historyApiFallback: true,
}Copy the code

Configuration entry file

The React Router is built on history. In short, a history knows how to listen for changes in the browser’s address bar, parse that URL into a Location object, and then the router uses it to match the route and render the corresponding component correctly. Such as:

import { browserHistory } from 'react-router'; import { syncHistoryWithStore } from 'react-router-redux'; const store = configureStore(); const history = syncHistoryWithStore(browserHistory, store); // Pass them to <Router> <Root store={store} history={history} />Copy the code
// ./src/index.js import { browserHistory } from 'react-router' import { syncHistoryWithStore } from 'react-router-redux' import Root from './containers/root' import configureStore from './store/configureStore' const store = configureStore() const history = syncHistoryWithStore(browserHistory, store) render( <Root store={store} history={history} />, document.getElementById('root') ); /** ./src/containers/root.dev.js */ import routes from '.. /config/route' import { Router } from 'react-router' const Root = ({ store, history }) => ( <Provider store={store}> <div> <Router history={history} routes={routes} /> </div> </Provider> ); Root.propTypes = { store: PropTypes.object.isRequired, history: PropTypes.object.isRequired }; // .src/config/route.js import React from 'react' import { Route } from 'react-router' import App from '.. /containers/app' const RouteConfig = ( <Route path="/" component={App}> </Route> ); export default RouteConfig;Copy the code

Configure the root file of the Reducers

// ./src/reducers/index.js
var { combineReducers } = require('redux');
import { routerReducer } from 'react-router-redux';
module.exports = combineReducers({
  routing: routerReducer,
});Copy the code

Routing has basically been configured. Now we can try to obtain RubyChina’s post data on the home page and render it to realize asynchronous loading.


3. Obtain data asynchronously

As mentioned earlier, it is best to encapsulate each case separately to reduce coupling when loading data asynchronously. We need to make three judgments about the data when calling the API:

  • A reducer action that informs the reducer that the request has begun
  • A reducer action that notifies the reducer that the request ended successfully.
  • An action that notifies the reducer that the request fails.

Let’s start with a generic fetch function:

import fetch from 'isomorphic-fetch'; If the server does not require cross-domain access, use the original address, such as: https://ruby-china.org/api/v3/. const url = 'https://localhost:8890/api/v3/' const urlTranslate = (tag) => { switch(tag) { case 'jobs': // the hiring node is node_id=25 topics return 'topics? Node_id =25' default: return 'topics'}} // fetchData = (tag, method = 'get', params = null); Promise<Action> => { const api = url + urlTranslate(tag); console.log(decodeURI(api)); return fetch(api, { method: method, body: params}) .then(response =>{ if (! response.ok) { return Promise.reject(response); } return Promise.resolve(response.json()); }). Catch (error => {return promise.reject (" server exception, please try again later "); })}Copy the code

This code encapsulates the callback to fetch data, and then we just need to call it in the action. I separate the successful request and the failed request into two action types, which can be determined by personal habits and generally keep the internal staff norms.

const fetchTopics = (tab) => dispatch => { dispatch(requestTopics(tab)) return fetchData(tab).then(response => { Dispatch (receiveTopics(TAB, response))}). Catch (error => {// Request data failed dispatch({type: 'RECEIVE_TOPICS_FAILURE', error: error, }) }) } const receiveTopics = (tab, json) => ({ type: 'RECEIVE_TOPICS_SUCCESS', tab, topics: json.topics, receivedAt: Date.now() })Copy the code

We initiate an action when we need to request data:

Const REQUEST_TOPICS = TAB => ({type: 'REQUEST_TOPICS', TAB})Copy the code

This last minute we update the state of isFetching in the Reducer to show that data has been loaded, and the user will determine if there is any fetching == true when requesting again. If there is any fetching condition, there will be no repeat fetching.

//.src/reducers/topics.js const initialState = { isFetching: false, didInvalidate: false, items: []} // update select TAB const selectedTab = (state = 'topics', action) => {switch (action.type) {case 'SELECT_TAB': return action.tab default: return state } } const topics = (state = initialState, action) => { switch (action.type) { case 'REQUEST_TOPICS': return { ... state, isFetching: true, } case 'RECEIVE_TOPICS_SUCCESS': return { ... state, isFetching: false, items: action.topics, lastUpdated: action.receivedAt } case 'RECEIVE_TOPICS_FAILURE': return { ... state, isFetching: false, err: action.error } default: return state } }Copy the code

Before updating data, we need to confirm whether the conditions for updating are met:

/**./ SRC /actions/topic.js */ / getState) => { if (shouldFetchTopics(getState(), tab)) { return dispatch(fetchTopics(tab)) } } const shouldFetchTopics = (state, TAB) => {// Select topicsByTab from topicsByTab; // Select TAB from topicsByTab; Const Topics = state.topicsByTab[TAB] if (! Topics) {return true} // Object exists and fetching new data if (topics. Isvalidate) {return false} return topics .src/reducers/topics.js */ const topicsByTab = (state = { }, action) => { switch (action.type) { case 'INVALIDATE_TAB': case 'RECEIVE_TOPICS_SUCCESS': case 'RECEIVE_TOPICS_FAILURE': case 'REQUEST_TOPICS': return { ... state, [action.tab]: topics(state[action.tab], action) } default: return state } }Copy the code

These are the main steps to get data asynchronously.


Now that we have a basic understanding of the project, let’s start working on RubyChina.