# React全家桶

# React

TIP

目前使用的是15.6.3 版本,后面会有新版本

# hello world

  • 首先我们不用任何脚手架,写一个 Hello world demo
  • 我对门首先到 github 上面下载 React 文件 网址
  • https://github.com/facebook/react/releases?after=v15.6.0 注意网址上后面跟了一个after参数指定了版本号,否则要next很多次才能找到对应版本的文件
  • 我们还可以使用npm的方式来下载对应的包,到引入的时候直接在node_modules中引入即可,需要注意版本号
  npm i react@xx.xx.xx react-dom@xx.xx.xx -S
1
  • 首先我们下载好react.jsreact-dom.js文件后,准备一个HTML文件并引入
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="container"></div>
  <script src="./react.js"></script>
  <script src="./react-dom.js"></script>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 接下来我们开始写React代码部分
<script>
  let DemoCompenent = React.createClass({
    render: function() {
      return React.createElement('h1',null,'hello world')
    }
  })
  ReactDOM.render(React.createElement(DemoCompenent),document.getElementById('container'))
</script>
1
2
3
4
5
6
7
8
  • ok第一个reactdemo完成

# 初探 jsx

  • 首先如果要使用jsx语法,我们需要一个工具来帮助浏览器来转换这样的语法babel-core
  • https://www.bootcdn.cn/babel-core/5.8.38/在这里可以下载,下载后引入
  • 还有一点需要注意,就是我们写js语法的地方需要在script标签中加入type="text/babel"属性
  • 接下来,我们看一下修改后的代码
<body>
  <div id="container"></div>
  <script src="./react.js"></script>
  <script src="./react-dom.js"></script>
  <script src="https://cdn.bootcdn.net/ajax/libs/babel-core/5.8.38/browser.js"></script>
  <script type="text/babel">
    let DemoCompenent = React.createClass({
      render: function(){
        return <h1>hello world</h1>
      }
    })
    ReactDOM.render(<DemoCompenent/>,document.getElementById('container'))
  </script>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 这里有一点需要注意,就是我们定义的DemoCompenent组件变量需要以大驼峰法(Camel-Case)进行定义,否则将不会成功,可以尝试一下小驼峰试一下

# 组件属性

  • 和正常的HTML标签一样,我们自定义的组件也可以传入属性,在组件之中就可以使用
  • 在组件之中可以通过this.props的方式使用属性
<script type="text/babel">
  let DemoCompenent = React.createClass({
    render: function(){
      return <h1>{this.props.talk ? this.props.talk : 'hello world'}</h1>
    }
  })
  ReactDOM.render(<DemoCompenent talk="husky are you scared"/>,document.getElementById('container'))
</script>
1
2
3
4
5
6
7
8

# jsx 渲染数组

  • 首先我们需要定义一个数组变量,然后再jsx中使用,通过Arraymap方法来遍历返回一个新的jsx数组
<script type="text/babel">
  let animals = ['fish', 'cat', 'husky']
  let DemoCompenent = React.createClass({
    render: function () {
      return <div>
          {
            animals.map((name)=>{
              return <p key={name}>{name}</p>
            })
          }
        </div>
      }
  })
  ReactDOM.render(<DemoCompenent talk="husky are you scared" />,document.getElementById('container'))
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 其实通过map遍历得来的就是
[ <p key="fish">fish</p>, <p key="cat">cat</p>, <p key="husky">husky</p> ]
1

# state 和 绑定事件

  • 通过 getInitialState 定义组件内状态
  • 通过setState修改组件内状态
  • 绑定事件
let DemoCompenent = React.createClass({
        getInitialState: function(){
          return {title:'husky'}
        },
        handleClick: function(){
          this.setState({
            title:'keji'
          })
        },
        render: function () {
          return <div>
              <button onClick={this.handleClick}>i like {this.state.title}</button>
            </div>
          }
      })
      ReactDOM.render(<DemoCompenent />,document.getElementById('container'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 虽然很简单,但是有一些细节需要注意,定义状态的getInitialState函数要有return返回值
  • 注册事件要用小驼峰命名方法,使用{}表达式的时候不用谢""

# 生命周期

  • componentWillMount 组件将准备挂载到真实dom
  • componentDidMount 组件已经挂载到真实dom
  • componentWillUpdate 组件状态将要更新的钩子
  • componentDidUpdate 组件状态已经更新的钩子
  • componentWillUnmount 组件将要销毁的钩子函数
  • 接下来我们用一个demo来演示
let DemoCompenent = React.createClass({
  getInitialState: function(){
    return {title:'husky'}
  },
  componentWillMount: function(){
    console.log(1);
  },
  componentDidMount: function(){
    console.log(2);
  },
  componentWillUpdate: function(){
    console.log(3);
  },
  componentDidUpdate: function(){
    console.log(4);
  },
  componentWillUnmount: function(){
    console.log(5);
  },
  handleClick: function(){
    this.setState({
      title:'keji'
    })
    setTimeout(()=>{
      ReactDOM.unmountComponentAtNode(document.getElementById('container'))
    },1000)
  },
  render: function () {
    return <div>
        <button onClick={this.handleClick}>i like {this.state.title}</button>
      </div>
    }
})
ReactDOM.render(<DemoCompenent talk="husky are you scared" />,document.getElementById('container'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  • 这里需要注意的是最后一个componentWillUnmount钩子函数,我们在点击事件中通过异步的方式执行了销毁的方法

# 子组件

  • 我们来用一个select标签 和 option标签来演示一下 react子组件的使用方式
  let DemoCompenent = React.createClass({
    render: function () {
      return(
        <select>
          { 
            React.Children.map(this.props.children, function (child) {
              return child // 就相当于是<option value="dog">dog</option>  
            })
          }
        </select>
      )
    }
  })
  ReactDOM.render(
    <DemoCompenent>
      <option value="dog">dog</option>  
      <option value="cat"> cat</option>  
      <option value="keji"> keji</option> 
    </DemoCompenent>,
    document.getElementById('container')
  )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 需要注意的是React.Childrenthis.props.childrenchildren大小写细节

# 属性校验和属性默认值

  • 通过propTypes进行属性校验,getDefaultProps增加属性默认值
  • 属性校验仅在开发模式给予warning提示,生产环境需要额外逻辑处理
let DemoCompenent = React.createClass({
        propTypes:{
          name:React.PropTypes.string.isRequired // name属性为string类型,切必须传入
        },
        getDefaultProps:function(){
          return {
            name:'two haha'
          }
        },
        render: function () {
          return <div>{this.props.name}</div>
        }
      })
      ReactDOM.render(<DemoCompenent name="husky"/>,document.getElementById('container'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 相当于VUE中的
props:{
  name:{
    default:'two haha',
    type: String
  }
}
1
2
3
4
5
6

# ref 获取真实dom

  • 这个和Vueref用法基本一致
let DemoCompenent = React.createClass({
  handleClick: function(){
    this.refs.box.style.background = '#ccc'
  },
  render: function () {
    return (
      <div ref="box">
        <h1 onClick={this.handleClick}>husky are you scared</h1>
      </div>
    )
    
  }
})
ReactDOM.render(<DemoCompenent/>,document.getElementById('container'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • Vue中是this.$refsreact中是this.refs

# 表单注意事项 bind

  • 主要注意表单绑定的事件,需要通过bind方法,来传递除了event的参数,如果不需要多余的参数传递进去,那么就正常绑定事件
  • label的for属性要写成htmlFor
let DemoCompenent = React.createClass({
  getInitialState: function(){
    return { name : '' }
  },
  handleChnage(name,e){
    this.setState({
      [name] : e.target.value
    })
  },
  handleClick: function(e){
    e.preventDefault()
    console.log(this.state.name);
  },
  render: function () {
    return (
      <form onSubmit={this.handleClick}>
        <label htmlFor="name"></label>
        <input type="text" onChange={this.handleChnage.bind(this,'name')} value={this.state.name}/>
        <input type="submit"/>
      </form>
    )
  }
})
ReactDOM.render(<DemoCompenent/>,document.getElementById('container'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 动态渲染 style

  style={{background:this.state.bg}} // 正确写法
  style="background:{this.state.bg}" // 错误写法
1
2
  • 下面这个示例,通过在输入框中输入RGB的值来动态改变父容器的背景色
let DemoCompenent = React.createClass({
  getInitialState: function () {
    return { bg: "" };
  },
  handleChnage(e) {
    this.setState({
      color: '#'+e.target.value,
    });
  },
  render: function () {
    return (
      <div style={{backgroundColor:this.state.bg}}>
          #<input
            type="text"
            onChange={this.handleChnage}
            value={this.state.bg}
          />
      </div>
    );
  },
});
ReactDOM.render(<DemoCompenent />, document.getElementById("container"));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 使用脚手架

  • 正常在工作中为了提高开发效率,项目的初始化,都使用脚手架来完成
  • 全局安装create-react-app
  npm install create-react-app -g
1
  • 开始构建一个项目
  create-react-app react-demo
1
  • 如果这个环节总是提示 There appears to be trouble with your network connection. Retrying...更换淘宝镜像就ok了
yarn config set registry https://registry.npm.taobao.org
1
  • 构建之后会产生如下文件
.gitignore
package.json
README.md
yarn.lock
node_modules
public
src
  App.css
  App.js
  App.test.js
  index.css
  index.js
  logo.svg
  serviceWorker.js
  setupTests.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • src下有很多文件,为了清晰的由浅入深,我们只留index.js文件,其余删除

# 16.x版本组件写法

  • 我们来改写一下index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById("root")
);
1
2
3
4
5
6
7
8
9
10
  • 新建一个app.js用来写组件
import React,{Component} from 'react';
class App extends Component {
  render(){
    return (
      <h1>first component</h1>
    )
  }
}
export default App;
1
2
3
4
5
6
7
8
9
  • 之前组件是通过 React.createClass 创建的

  • 还有另一种写组件的方式,称为无状态组件,上面这种成为有状态组件

  • 但是后面会出React Hooks ,这个概念也就不存在了

import React from 'react';
function App2(){
  return (
    <h1>无状态组件</h1>
  )
}
export default App2;
1
2
3
4
5
6
7

# jsx 踩坑

  • 在15.x版本中,已经提到过jsx的基本使用,在这里记录一下踩坑

# 注释

  • 注释是要写在{}中
 {/* jsx中的注释写法 */}
1

# class需要改写为className

<h1 className="text">class注意</h1>
1

# for 需要改写为 htmlFor

# 通过 dangerouslySetInnerHTML 解析HTML元素

class App extends Component {
  state={
    title:'<h1>hello world</h1>'
  }
  render(){
    return (
      <div>
        <h1 dangerouslySetInnerHTML={{ __html: this.state.title }}/>
      </div>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12

# 父子组件传值

  • 首先我们先定义两个组件,首先定义子组件
import React, { Component } from 'react';
class Child extends Component {
  constructor (props) {
    super(props)
    this.handleDelete = this.handleDelete.bind(this)
  }
  render() {
    return <p onClick={this.handleDelete}>{this.props.name}</p>;
  }
  handleDelete(){
    this.props.handleDelete(this.props.index)
  }
}
export default Child;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 其中handleDelete是通过父组件传递过来的方法
  • 再定义一个父组件来使用子组件,并传入子组件需要的属性
import React, { Component } from "react";
import Child from "./Child";

class App extends Component {
  state = { list: ["dog", "cat", "fish"] };
  constructor(props) {
    super(props);
    this.handleDelete = this.handleDelete.bind(this);
  }
  render() {
    return (
      <div>
        {this.state.list.map((item, index) => {
          return (
            <div key={item}>
              <Child
                index={index}
                handleDelete={this.handleDelete}
                name={item}
              ></Child>
            </div>
          );
        })}
      </div>
    );
  }
  handleDelete(index) {
    this.state.list.splice(index, 1)
    this.setState({
      list: this.state.list,
    });
  }
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

# propTypes 检验prop传值类型

  • 需要在使用prop的子组件中来校验
  • 需要引入prop-types工具包,它里面有很多配置,这里我们简单举两个例子
  • 通过当前类中的属性propTypes来配置
import React, { Component } from 'react';
import PropTypes from 'prop-types';

class Child extends Component {
  constructor (props) {
    super(props)
    this.handleDelete = this.handleDelete.bind(this)
  }
  render() {
    return <p onClick={this.handleDelete}>{this.props.name}</p>;
  }
  handleDelete(){
    this.props.handleDelete(this.props.index)
  }
}

Child.propTypes = {
  handleDelete:PropTypes.func.isRequired,
  name:PropTypes.string.isRequired
};

export default Child;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# defaultProps定义默认值

  • 如果我们父组件没有给对应的属性传值,那么子组件将会使用默认值
  • 通过当前类中的属性defaultProps配置
Child.defaultProps = {
  name:'animal'
};
1
2
3

# 生命周期性能优化

  • 通过shouldComponentUpdate生命周期函数来判断属性是否变换,如果没变不进行重新渲染
shouldComponentUpdate(nextProps,nextState) {
    if(nextProps.name!==this.props.name){
      return true
    }else {
      return false
    }
  }
  render() {
    return <p onClick={this.handleDelete}>{this.props.name}</p>;
  }
1
2
3
4
5
6
7
8
9
10

# Redux

# 准备环节

  • 首先我们需要安装redux
yarn add redux -S
1
  • 在使用之前,我们先写一些UI来配合使用redux,我们安装antd UI
yarn add antd -S
1

# 界面搭建

  • 我们用一个常用案例TodoList 来演示
import React, { Component } from 'react'
import 'antd/dist/antd.css'
import { Input, Button, Card, Divider } from "antd"

class App extends Component {
  constructor(props) {
    super(props);
    this.state = { 
      list: []
    }
  }
  render() { 
    return (
      <div style={{ width: "50%", margin: "20px auto" }}>
        <div style={{ display: "flex", justifyContent: "space-between"}}>
          <Input style={{ width: "80%" }} />
          <Button type="primary">添加</Button>
        </div>
        <Divider dashed />
          {this.state.list.map((item, index) => {
            return (
              <Card key={index + 1} style={{marginTop: 10}}>
                <p>{item}</p>
              </Card>
            )
          })}
      </div>
    )
  }
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

# 使用 redux devtools

  • Google商店下载,或者去GitHub下载redux-devtools-extension
  • 代码中要写一段逻辑配合redux devtools起效果


 


const store = createStore(
  reducer,
+ window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
1
2
3
4

# 创建createStore、reducer

  • src中新建store目录
  • store中新建index.js 和 新建 reducer.js
// index.js
import {createStore} from 'redux'
import reducer from './reducer';

const store = createStore(reducer);

export default store
1
2
3
4
5
6
7
  • 这里我们给一个默认值
// reducer.js
let defaultState = {
  value: "",
  list: [1, 2, 3],
};
export default (state = defaultState, action) => {
  return state;
};

1
2
3
4
5
6
7
8
9

# 给UI绑定点击事件,并触发store.dispatch来改变数据

  • 接下来我们对上面的Todo代码进行改进
class App extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState(); // 初始化数据状态
    this.handleInputChange = this.handleInputChange.bind(this);
    this.storeChange = this.storeChange.bind(this);
    this.handleAddClick = this.handleAddClick.bind(this);
    store.subscribe(this.storeChange); // 对store进行订阅,一旦数据改变,调用storeChange改变组件内状态
  }
  render() {
    return (
      <div style={{ width: "50%", margin: "20px auto" }}>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <Input style={{ width: "80%" }} value={this.state.value} onChange={this.handleInputChange} />
          <Button type="primary" onClick={this.handleAddClick}>
            添加
          </Button>
        </div>
        <Divider dashed />
        {this.state.list.map((item, index) => {
          return (
            <Card
              onClick={this.handleDeleteClick.bind(this, index)}
              key={index + 1}
              style={{ marginTop: 10 }}
            >
              <p>{item}</p>
            </Card>
          );
        })}
      </div>
    );
  }

  // 监听输入框的值变化
  handleInputChange(e) {
    const action = {
      type: "changeInput",
      value: e.target.value,
    };
    store.dispatch(action);
  }

  // 增加一项
  handleAddClick() {
    const action = {
      type: "add"
    };
    store.dispatch(action);
  }
  // 删除一项
  handleDeleteClick(index) {
    console.log(arguments);
    const action = {
      type: "delete",
      value: index,
    };
    store.dispatch(action);
  }
  // 数据改变 同时 改变组件内状态
  storeChange() {
    this.setState(store.getState());
  }
}
export default App;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

# 修改reducer中的action逻辑

  • 我们需要给对应的dispatch传递的action进行处理,来对数据进行修改
export default (state = defaultState, action) => {
  let newState = JSON.parse(JSON.stringify(state));
  switch (action.type) {
    case 'changeInput':
      newState.value = action.value
      return newState
    case 'add':
      newState.list.push(newState.value)
      newState.value =''
      return newState
    case 'delete':
      newState.list.splice(action.value, 1);
      return newState
    default:
      break
  }
  return state
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 优化 action 和 actionType

  • 首先我们新建两个文件 actionTypesactionCreators 两个文件
  • 先来声明actionType常量
// actionTypes
export const CHANGE_INPUT = 'changeInput'
export const DELETE_ITEM = 'deleteItem'
export const ADD_ITEM = 'addItem'
1
2
3
4
  • 在抽离action的逻辑
// actionCreators
import { CHANGE_INPUT, DELETE_ITEM, ADD_ITEM } from "./actionTypes"

export const changeInputAction = (value)=>{
  return {
    type: CHANGE_INPUT,
    value
  }
}
export const addItemAction = () => {
  return {
    type: ADD_ITEM,
  };
};

export const deleteItemAction = (value) => {
  return {
    type: DELETE_ITEM,
    value,
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
  • 做了上面的优化,我们来改写一下Todo的绑定事件代码,将action抽离出来单独管理

import { changeInputAction, addItemAction, deleteItemAction } from './store/actionCreators';


  handleInputChange(e) {
    const action = changeInputAction(e.target.value)
    store.dispatch(action);
  }
  handleAddClick() {
    const action = addItemAction()
    store.dispatch(action);
  }
  handleDeleteClick(index) {
    const action = deleteItemAction(index)
    store.dispatch(action);
  }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 优化:将UI和业务逻辑分离

  • 首先我们在src目录下新建一个ToDoUI.js来放UI代码
  • 将原来的App.js中的UI部分抽离出来
// ToDoUI.js
import React, { Component } from 'react'; 

import { Input, Button, Card, Divider } from "antd";
class ToDoUI extends Component {

  render() { 
    return (
      <div style={{ width: "50%", margin: "20px auto" }}>
        <div style={{ display: "flex", justifyContent: "space-between" }}>
          <Input
            style={{ width: "80%" }}
            value={this.props.value}
            onChange={this.props.handleInputChange}
          />
          <Button type="primary" onClick={this.props.handleAddClick}>
            添加
          </Button>
        </div>
        <Divider dashed />
        {this.props.list.map((item, index) => {
          return (
            <Card
              onClick={() => {this.props.handleDeleteClick(index)}}
              key={index}
              style={{ marginTop: 10 }}
            >
              <p>{item}</p>
            </Card>
          );
        })}
      </div>
    );
  }
}
export default ToDoUI;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • UI组件中需要的属性和方法将通过属性的方式传递过来
  • 那么接下来改造业务逻辑部分,没有太大变化,主要是讲UI部分单独提出去,在这里面已组件的方式引入,并传递需要的props
import React, { Component } from 'react'
import store from './store';
import { changeInputAction, addItemAction, deleteItemAction } from './store/actionCreators';
import ToDoUI from './ToDoUI'

class App extends Component {
  constructor(props) {
    super(props);
    this.state = store.getState();
    this.handleInputChange = this.handleInputChange.bind(this);
    this.storeChange = this.storeChange.bind(this);
    this.handleAddClick = this.handleAddClick.bind(this);
    this.handleDeleteClick = this.handleDeleteClick.bind(this);
    store.subscribe(this.storeChange);
  }
  render() {
    return (
      <ToDoUI
        value={this.state.value}
        handleInputChange={this.handleInputChange}
        handleAddClick={this.handleAddClick}
        handleDeleteClick={this.handleDeleteClick}
        list={this.state.list}
      />
    );
  }
  handleInputChange(e) {
    const action = changeInputAction(e.target.value)
    store.dispatch(action);
  }
  handleAddClick() {
    const action = addItemAction()
    store.dispatch(action);
  }
  handleDeleteClick(index) {
    console.log(index);
    const action = deleteItemAction(index)
    store.dispatch(action);
  }
  storeChange() {
    this.setState(store.getState());
  }
}
export default App;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# 优化:UI组件改为无状态组件

  • 无状态组件通过function方式定义并将jsxUI部分return即可
  • props 作为function的形参传入
import React, { Component } from 'react'; 
import { Input, Button, Card, Divider } from "antd";

function ToDoUI(props){
  return (
    <div style={{ width: "50%", margin: "20px auto" }}>
      <div style={{ display: "flex", justifyContent: "space-between" }}>
        <Input
          style={{ width: "80%" }}
          value={props.value}
          onChange={props.handleInputChange}
        />
        <Button type="primary" onClick={props.handleAddClick}>
          添加
        </Button>
      </div>
      <Divider dashed />
      {props.list.map((item, index) => {
        return (
          <Card
            onClick={() => {props.handleDeleteClick(index)}}
            key={index}
            style={{ marginTop: 10 }}
          >
            <p>{item}</p>
          </Card>
        );
      })}
    </div>
  )
}
export default ToDoUI;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

# redux-thunk中间件

  • 使用redux-thunk可以让您编写与store交互的异步逻辑
  • 首先安装redux-thunk
yarn add redux-thunk -S
1
  • 接下来去store目录中使用redux-thunk
import {createStore, applyMiddleware} from 'redux'
import reducer from './reducer';
import thunk from 'redux-thunk';
const store = createStore(reducer, applyMiddleware(thunk));
1
2
3
4
  • 因为我们要适配redux-devtools所以要引入compose,来实现增强函数
import {createStore, compose, applyMiddleware} from 'redux'
import reducer from './reducer';
import thunk from 'redux-thunk';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
  ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({})
  : compose

const enhancer = composeEnhancers(applyMiddleware(thunk));

const store = createStore(reducer, enhancer);

export default store
1
2
3
4
5
6
7
8
9
10
11
12

# redux-thunk 示例

  • 首先我们需要准备模拟请求的网站,这里我们使用一言提供的接口
  • 一言提供了接口可以根据传递不同参数返回不同的数据,这里我们已最简单的接口进行演示
  • get 请求 https://v1.hitokoto.cn/,返回数据格式如下
{
"id": 5859,
"uuid": "df187911-1d7f-46e5-8c62-ad156d9227f0",
"hitokoto": "合抱之木,生于毫末;九层之台,起于累土;千里之行,始于足下。",
"type": "i",
"from": "老子·德经·第六十四章",
"from_who": null,
"creator": "a632079",
"creator_uid": 1044,
"reviewer": 4756,
"commit_from": "web",
"created_at": "1586398126",
"length": 30
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 接下来我们在actionTypes中声明请求列表的action Type
+ export const GET_LIST = 'getList'
1
  • actionCreators中声明action
  • 这里我们要写两个,一个是异步请求数据的action,一个是改变触发store更新的action
export const getList = (value) => {
  return {
    type: GET_LIST,
    value
  };
};

export const getAsyncList = (value) => {
  return (dispatch)=>{
    Axios.get("https://v1.hitokoto.cn/").then(res=>{
      let action = getList(res.data)
      dispatch(action)
    })
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 准备工作都做好了,接下来在Todo的业务逻辑层的componentDidMount中执行
import { /*...*/, getAsyncList } from './store/actionCreators';
componentDidMount() {
    let action =  getAsyncList()
    store.dispatch(action)
  }
1
2
3
4
5

# 使用react-redux

  • 首先进入项目的入口处全局增加Provider
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
import "antd/dist/antd.css";
+ import {Provider} from 'react-redux'
+ import store from './store'

+ const main= (
+   <Provider store={store}>
+     <App />
+   </Provider>
+ )
ReactDOM.render(main, document.getElementById("root"));
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 接下来我们再去ToDoUI文件中改造
  • 主要改造部分就是export default的时候connect一下,主要作用,就是将原来通过props传递过来的属性在这里进行一次转换
  • 通过 connect 的 第一个参数 stateToProps,将state中的属性转换为当前组件props的属性
  • 通过 connect 的 第二个参数 dispatchToProps ,将父组件中触发dispatch的函数挪到这一层,不需要父组件传递相应dispatch方法,直接通过connect的方式在props上增加相应的方法
  • 也就是说,父组件中不需要给当前组件传递任何值,就可以完成之前的todo交互
const stateToProps = (state)=>{
  return {
    value: state.value, 
    list: state.list
  };
}
const dispatchToProps = (dispatch)=>{
  return {
    handleInputChange(e) {
      let action = changeInputAction(e.target.value);
      dispatch(action);
    },
    handleAddClick(e) {
      let action = addItemAction();
      dispatch(action);
    },
    initGetAsyncList() {
      let action = getAsyncList();
      dispatch(action);
    },
    handleDeleteClick(index) {
      let action = deleteItemAction(index);
      dispatch(action);
    },
  };
}

export default connect(stateToProps, dispatchToProps)(ToDoUI);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 可以删除 给<ToDoUI>组件传递的属性了,和dispatch还有store的逻辑都可以删掉了
// ... 省略 ...
render() {
    return (
      <ToDoUI
-       value={this.state.value}
-       handleInputChange={this.handleInputChange}
-       handleAddClick={this.handleAddClick}
-       list={this.state.list}
-       handleDeleteClick={this.handleDeleteClick}
      />
    );
  }
// ... 省略 ...
1
2
3
4
5
6
7
8
9
10
11
12
13

# React Router

# 安装 react-router

yarn add react-router -S
1

# 按照传统,准备组件资源

  • 首先写着我们上面写ToDo的例子写
  • 新建一个other.js无状态组件
import React from 'react';
function Other () {
  return (
    <h1>other page</h1>
  )
}
export default Other;
1
2
3
4
5
6
7
  • 新建一个AppRouter.js文件用来定义菜单组件,我们主要的路由就是在这里面配置
import React, { Component } from "react"; 
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
import { Menu } from 'antd';
import ToDo from "./ToDo";
import Other from "./other";

class AppRouter extends Component {
  state = {
    current: 'mail',
  };

  handleClick = e => {
    console.log('click ', e);
    this.setState({ current: e.key });
  };
  render() {
    const { current } = this.state;
    return (
      <Router>
        <Menu
          onClick={this.handleClick}
          selectedKeys={[current]}
          mode="horizontal"
        >
          <Menu.Item key="mail">
            <Link to="/">todo</Link>
          </Menu.Item>
          <Menu.Item key="app">
            <Link to="/other/">other</Link>
          </Menu.Item>
        </Menu>
        <Route path="/" exact component={ToDo} />
        <Route path="/other/" component={Other} />
      </Router>
    );
  }
}
export default AppRouter;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
  • 我们通过Router、Route、Link三个组件的配合,完成了最简单的路由配置

# 动态传值

  • 首先设置动态传值的规则,在Route组件上
  • 我们在path中使用:xxx的方式设置动态路由参数
<Route path="/" exact component={ToDo} />
<Route path="/other/:title" component={Other} />
1
2
  • 修改ToDo的代码,加上Link标签来跳转到other并动态传入参数
  {props.list.map((item, index) => {
    return (
      <Card
        onClick={() => {props.handleDeleteClick(index)}}
        key={index}
        style={{ marginTop: 10 }}
      >
        <p>{item}</p>
+       <Link to={`/other/`+item}>查看详情</Link>
      </Card>
    );
  })}
1
2
3
4
5
6
7
8
9
10
11
12
  • 动态路由传入后,对应的路由页面可以在props中拿到传入的参数
  • 具体在 propsmatch属性的params属性中,在具体取值就是我们定义规则的时候:xxx的变量名
import React from 'react';
function Other(props) {
  return <h1>title:{props.match.params.title}</h1>;
}
export default Other;
1
2
3
4
5

# redirect

  • react-router-dom中引入Redirect组件进行重定向
function Other(props) {
  return (
    <div>
      <Redirect to="/"></Redirect>
      <h1>title:{props.match.params.title}</h1>;
    </div>
  );
}
1
2
3
4
5
6
7
8
  • 通过js props.history.push的方式进行重定向
function Other(props) {
  props.history.push('/')
  return (
    <div>
      <h1>title:{props.match.params.title}</h1>;
    </div>
  );
}
1
2
3
4
5
6
7
8

# 嵌套路

  • 嵌套路由很简单,就是在第一层Route路由的组件中重新再配置一套Route规则即可
  • page1page2为简单的为无状态组件
import React, { Component } from 'react';
import { Menu } from "antd";
import {Route,Link} from "react-router-dom"
import page1 from './views/page1';
import page2 from './views/page2';
const { SubMenu } = Menu;

class other extends Component {
  handleClick = (e) => {
    console.log("click ", e);
  };
  render() {
    return (
      <div>
        <Menu
          onClick={this.handleClick}
          style={{ width: 256 }}
          mode="vertical"
        >
          <SubMenu
            key="sub1"
            title={
              <span>
                <span>Navigation One</span>
              </span>
            }
          >
            <Menu.Item key="1">
              <Link to="/other/page1">page1</Link>
            </Menu.Item>
            <Menu.Item key="2">
              <Link to="/other/page2">page2</Link>
            </Menu.Item>
          </SubMenu>
        </Menu>
        <div className="childRoute">
          <Route path="/other/page1" component={page1} />
          <Route path="/other/page2" component={page2} />
        </div>
      </div>
    );
  }
}
export default other
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44

# React Hook

# useState

  • 声明带有状态的变量
  • 通过数组解构的方式得到状态变量,和这个状态改变得方法
import React, {  useState } from 'react';
function Page1 () {
  let [attr1, setAttr1] = useState({
    name:'husky',
    age:2
  });
  return (
    <h1 onClick={()=>{setAttr1({ name: "two haha", age: 3 });}}>
      {attr1.name}
      {attr1.age}
    </h1>
  );
}
export default Page1;
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# useEffect(一)

  • 代替有状态组件中的componentDidMountcomponentDidUpdate钩子函数
  • 第一次挂在Dom完成的时候执行一次,数据更新之后执行一次
function Page1 () {
  let [attr1, setAttr1] = useState({
    name:'husky',
    age:2
  });
  useEffect(()=>{
    console.log("attr1");
  })
  return (
    <h1 onClick={()=>{setAttr1({ name: "two haha", age: 3 });}}>
      {attr1.name}
      {attr1.age}
    </h1>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# useEffect(二)

  • 通过useEffect 模拟 componentWillUnmount
  • 通过在useEffect第一个匿名函数参数中return一个匿名函数的方式来执行,当然还需要在useEffect的第二个参数中设置空数组
  • 如果不设置空数组,就代表,只要这个组件内有数据变化,就会再执行一遍
  • 如果在数组中写了某些状态,那就代表只有写在数组中的状态改变时,才会在触发useEffect
function Page1 () {
  let [attr1, setAttr1] = useState({
    name:'husky',
    age:2
  });
  useEffect(()=>{
    console.log("attr1");
    return ()=>{
      console.log('goodbye');
    }
  })
  return (
    <h1 onClick={()=>{setAttr1({ name: "two haha", age: 3 });}}>
      {attr1.name}
      {attr1.age}
    </h1>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 当切换路由组件销毁的时候就会在控制台打印goodbye

# useContext父子组件通信

  • 通过createContext 在父组件中定义并且提供给子组件的状态
export const parentContext = createContext() 

return (
  <div>
    <parentContext.Provider value={/*父组件要共享给子组件的状态*/}>
      {/*子组件*/}
    </parentContext.Provider>
  </div>
)
1
2
3
4
5
6
7
8
9
  • 在子组件中用 useContext来接收父组件传递的状态
import { parentContext } from "从父组件中引入createContext创建变量";

let name = useContext(parentContext);

return (<h1>{name}</h1>)
1
2
3
4
5

# useReducer

  • 类似于redux中的reducer的功能
  • 接下来使用一个加减数值的案例来了解一下useReducer的使用方式
import React, { useReducer } from 'react';

function Page1 () {
  let [count, dispatch] = useReducer((state, action)=>{
    console.log(action);
    switch (action.type) {
      case 'ADD':
        return ++state;
      case 'SUB':
        return --state;
      default:
        return state
    }
  }, 0)
  return (
    <div>
      <h1>{count}</h1>
      <button
        onClick={() => {
          dispatch({ type: "ADD" });
        }}
      >
        +
      </button>
      <button
        onClick={() => {
          dispatch({ type: "SUB" });
        }}
      >
        -
      </button>
    </div>
  );
}
export default Page1;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

# useContext配合useReducer实现状态管理

  • 首先我们将currentContext部分的逻辑单独抽离出来
import React, { createContext } from 'react';

export const currentContext = createContext({});

export const ContextContainer = (props)=> {
  return (
    <currentContext.Provider value={{bg:'#333'}}>
      {props.children}
    </currentContext.Provider>
  )
}
1
2
3
4
5
6
7
8
9
10
11
  • 接下来对currentContext中的代码进行改造,增加reducer部分逻辑
import React, { createContext, useReducer } from "react";

export const currentContext = createContext({});

export const ContextContainer = (props)=> {

  let [bg, dispatch] = useReducer(reducer, '#FFF');

  return (
    <currentContext.Provider value={{ bg, dispatch }}>
      {props.children}
    </currentContext.Provider>
  );
}
export const THEME_COLOR_LIGHT = "THEME_COLOR_LIGHT";
export const THEME_COLOR_DARK = "THEME_COLOR_DARK";

const reducer = (state, action) => {
  console.log(state);
  switch (action.type) {
    case "THEME_COLOR_DARK":
      return "#333";
    case "THEME_COLOR_LIGHT":
      return "#FFF";
    default:
      return state;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 接下来我们去使用,首先我们声明一个控制按钮组,来控制明暗模式
  • Context文件就是我们最开始声明的一些方法和属性以及组件
import React, { useContext } from "react";
import {
  currentContext,
  THEME_COLOR_DARK,
  THEME_COLOR_LIGHT,
} from "../Context";

function Ctrl() {
  let { dispatch } = useContext(currentContext);
  return (
    <div>
      <button
        onClick={() => {
          dispatch({ type: THEME_COLOR_DARK });
        }}>
        dark
      </button>
      <button
        onClick={() => {
          dispatch({ type: THEME_COLOR_LIGHT });
        }}>
        light
      </button>
    </div>
  );
}

export default Ctrl;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
  • 接下来处理显示明暗主题的组件,这个组件主要通过useContext来使用改变的状态
import React, { useContext } from "react";
import { currentContext } from "../Context";
function Platform() {
  let { bg } = useContext(currentContext);
  return <div style={{ width: 200, height: 200, background: bg }}></div>;
}

export default Platform;
1
2
3
4
5
6
7
8
  • 两个组件都定义玩了,还需要将他们添加到起初声明的createContext组件中
import React from "react";
import Platform from "./platform";
import Ctrl from "./ctrl";
import { ContextContainer } from "../Context";
function Page1() {
  return (
    <div>
      <ContextContainer>
        <Platform />
        <Ctrl />
      </ContextContainer>
    </div>
  );
}
export default Page1;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  • 至此我们模拟redux,改变元素主题色的小案例完成

# useMemo

  • 通过这个钩子函数可以解决,当某些组件的状态没有变化时,则这些组件内部逻辑不需要重新执行,相当于缓存
import React, { useMemo } from 'react';
function Page2({ title }) {
  function upperName(title = '') {
    return title.toUpperCase();
  }
  const result = useMemo(() => upperName(title), [title]);
  return (
    <>
      <h1>{result}</h1>
    </>
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
  • 通过上面这个例子,也就是说,只要title值不变,upperName方法就不会在执行第二遍了

# useRef

  • 通过useRef来获取来获取dom元素,通过useRef声明的dom变量会将dom的信息保存在该变量的current属性中
import React, { useRef } from 'react';
function Page2() {
  let h1El = useRef()
  console.log(h1El); // h1El.current
  return (
    <>
      <h1 ref={h1El}>husky are you scared</h1>
    </>
  );
}
export default Page2;
1
2
3
4
5
6
7
8
9
10
11

# custom hooks

  • 自定义钩子函数,主要就是将一些公共的方法抽离出来,降低耦合性
  • 需要注意的是自定义钩子函数,需要以use开头,就像useState、useContext一样
  • 接下来使用一个小案例来演示,主要作用就是,判断local storage中是否userInfo信息,如果没有跳转到登录的路由
import React, { useState ,useEffect } from 'react';

function useLoginState(props){
  let [loginState,setState] = useState(false);
  useEffect(()=>{
    if (localStorage.getItem("USER_INFO")) {
      setState(true)
    } else {
      props.history.push("/login/");
      setState(false);
    }
  }, [props.history])
  
  return loginState;
}

function Page2(props) {
  let h1El = useLoginState(props);
  console.log(h1El);
  
  return (
    <>
      <h1>husky are you scared</h1>
    </>
  );
}
export default Page2;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  • 这里为了演示,放到了一起,在项目中可以单独抽离成公共的hook
Last Updated: 1/23/2022, 10:16:22 AM