对react、redux的理解

react&redux从相识到相知


相识

  • 2016年初,自学meteor时,想顺便学习react
  • 写react上的todolist例子,配合meteor的后端实现功能(感觉自己好牛)

那时候写的代码


那时候写的代码


尝试运行,但卡在这里


  • 突然有人说react要用redux,这样写太乱了
  • Google redux,看到了很多概念:action、reducer、containers、store……(什么鬼)
  • 尝试按照官网的步骤用redux去改进todolist的demo,但并不理解,最终只是看看(感觉自己好菜)
  • 去Google开发者大会被Angular2和Typescript吸引,移情别恋:Angular2内容分享

  • 放弃react&redux


一个机会

  • 去战略合作部交流学习,又让我遇到了react&redux

看到了这样的代码

component

import React, { PropTypes } from 'react'
import { Checkbox, Tabs } from 'antd'
import moment from 'moment'
import orderBy from 'lodash/orderBy'
const ChatMessage = ({
  latestMessageList,
  openChatWindow,
  updateRoomLastSeenAt,
  clearMsgCount }) => (
    <ChatMessagePanel>
      <TabsWithStyle defaultActiveKey="1">
        <TabPane tab="患者消息" key="1">
          {latestMessageList &&
            orderBy(formatMessageListTime(latestMessageList), ['orderTime'], ['desc']).map(o => (
              <ChatMessageItem onClick={() => {
                openChatWindow(o.patientId); updateRoomLastSeenAt(o.roomId); clearMsgCount(o.roomId)
              }}
              >
                <ChatMessageName>{o.name}</ChatMessageName>
                <ChatMessageAvatar src={o.avatar} />
                <ChatMessageAccount>
                  <ChatMessageInfo>
                    {o.gender} | {o.age}
                  </ChatMessageInfo>
                  <ChatStyBadge count={o.unreadMessageCount} />
                  <ChatMessageDate>{moment(o.createdAt).format('YYYY-MM-DD HH:mm')}</ChatMessageDate>
                </ChatMessageAccount>
                <ChatMessageBrief>{o.text}</ChatMessageBrief>
              </ChatMessageItem>
            ),
            )}
        </TabPane>

        <TabPane tab="同事消息" key="2" />
      </TabsWithStyle>
      <ChatMessageMore />
      <ChatMessageMute>
        <Checkbox />
        <ChatMessageMuteNotice>患者群消息免打扰</ChatMessageMuteNotice>
      </ChatMessageMute>
    </ChatMessagePanel>
  )
ChatMessage.propTypes = {
  latestMessageList: PropTypes.array.isRequired,
  openChatWindow: PropTypes.array.isRequired,
  updateRoomLastSeenAt: PropTypes.func.isRequired,
  clearMsgCount: PropTypes.func.isRequired,
}

export default ChatMessage

container

import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { graphql, compose } from 'react-apollo'
import isEqual from 'lodash/isEqual'
import ChatMessageComponent from '../components/ChatMessage'
import { queryLatestMessages, openLatestMessageList, openChatWindow, clearMsgCount, updateMsgLastSeenAt } from '../actions/chatView'

const mapStateToProps = state => ({
  latestMessageList: state.chat.latestMessageList,
  count: state.chat.count,
})

const updateRoomLastSeenAt = graphql(updateMsgLastSeenAt, {
  props: ({ mutate }) => ({
    updateRoomLastSeenAt(chatRoomId) {
      console.log('chatRoomId:', chatRoomId)
      return mutate({
        variables: {
          chatRoomId,
        },
      })
    },
  }),
})
export default compose(updateRoomLastSeenAt,
  connect(mapStateToProps, {
    openLatestMessageList,
    openChatWindow,
    clearMsgCount,
  }),
  graphql(queryLatestMessages),
)(ChatMessageComponent)

reducers

import uniqWith from 'lodash/uniqWith'
import isEqual from 'lodash/isEqual'
import omit from 'lodash/omit'

const initialState = {
  unReadMessageCount: 0,
  patients: {},
}

const initialChatRoomState = {
  chatRoomId: null,
  chatRoomName: '',
  unReadMessageCount: 0,
  latestMessageCreatedAt: null,
  participants: [],
  status: 'OPEN',
  messages: [],
}

const chatViewReducer = (state = initialState, action) => {
  const { patientId } = action
  let count = 0
  switch (action.type) {
    case 'UPDATE_CHAT_ROOM': {
      const chatRooms = state.patients
      const existsChatRoom = chatRooms[patientId]
      if (existsChatRoom) {
        const mergeMessages = uniqWith(
          [
            ...action.messages.sort((a, b) => a.createdAt > b.createdAt),
            ...state.patients[patientId].messages,
          ],
          isEqual,
        )
        return {
          ...state,
          patients: {
            ...state.patients,

              ...state.patients[patientId],
              messages: mergeMessages,
              chatRoomId: action.chatRoomId,
              chatRoomName: action.chatRoomName,
            },
          },
        }
      }
      return state
    }
     case 'CLEAR_MSG_COUNT':
      return {
        ...state,
        latestMessageList: state.latestMessageList.map((item) => {
          if (item.roomId === action.roomId) {
            count = item.unreadMessageCount
            return {
              ...item,
              unreadMessageCount: 0,
            }
          }
          return item
        }),
        allUnreadCount: state.allUnreadCount - count,
      }

    default:
      return state
  }
}

export default chatViewReducer

内心OS


相知

  • 重新看文档写todolist的demo
  • 领任务,通过项目逼自己弄明白

很重要的概念props和states

  • props是一个可以从父组件传递给子组件的对象,这个对象可以一直传递到子孙组件。
  • state代表的是一个组件内部自身的状态(状态机)

是不是还是没懂😂

那我说的通俗一点


怎样理解props

  • props 就是组件中要用到的对象,这个对象能传递到另一个组件,或者从其他地方传过来

比如这样:

const element = <Welcome name="Sara" />;
function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

const element = <Welcome name="Sara" />;
ReactDOM.render(
  element,
  document.getElementById('root')
);

在这里name就是props


还可以是这样

const Todo = ({onClick, completed, text}) => (
    <li
       
        style={{
        textDecoration: completed
            ? 'line-through'
            : 'none'
    }}>
    <input type="checkbox" onClick={onClick} />
        {text}
    </li>
)

这里 onClick、completed、text 都是props


  • 一切你要传给其他组件的对象都是props


怎样理解state

  • state被用来控制组件的UI变化

如这样的


这样的


还有这样的


  • 组件的任何UI改变都可以从State的变化中反映出来。

props和state的关系

  • 通常需要根据state变化改变props,如点击某一个todo前面的CheckBox,这项todo要增加删除线(改变Style props)

两个要注意的地方

  • props是只读的,改变props只能通过此组件的父组件修改
  • 不能直接修改State,不能直接修改State,不能直接修改State!
    • 直接修改state,组件并不会重新触发render
// 错误
this.state.title = 'React';
// 正确
this.setState({title: 'React'});

redux解决了什么问题?

  • 大型项目state很多,管理不断变化的state非常困难,Redux 试图让 state 的变化变得可预测
  • state不再只属于某一组件,提升state,将state放到共有的父组件中来管理,再作为props分发回子组件。

三大原则

  • 单一数据源
    • 整个应用的 state 被储存在一棵 object tree 中,并且只存在于唯一一个 store 中
  • State 是只读的
    • 唯一改变 state 的方法就是触发 action
  • 使用纯函数来执行修改
    • 为了描述 action 如何改变 state tree ,需要编写 reducers

Action

  • Action 是把数据从应用传到 store 的有效载荷(Payload)
  • Action是JavaScript普通对象,描述动作
function addTodo(text) {
  return {
    type: ADD_TODO,
    text
  }
}

reducer

  • Action的处理
  • 接收旧的state和action,返回新的state

reducer

function todoApp(state = initialState, action) {
  switch (action.type) {
    case SET_VISIBILITY_FILTER:
      return Object.assign({}, state, {
        visibilityFilter: action.filter
      })
    case ADD_TODO:
      return Object.assign({}, state, {
        todos: [
          ...state.todos,
          {
            text: action.text,
            completed: false
          }
        ]
      })
    default:
      return state
  }
}

store.dispatch

  • 用来发起一个action

  • 这些都有了,如果把他们和UI联系在一起?

  • 在containers容器组件中使用connect()(myComp)

举个🌰

  • 还是那个很俗的todo项目😂


不使用redux如何实现

TodoHeader.js

import React from "react";
class TodoHeader extends React.Component {
    // 添加新任务
    handlerAddToDo(value){
        if(!value) return false;
        let newTodoItem = {
            text: value,
            isDone: false
        };
        // event.target.value = "";
        this.props.addTodo(newTodoItem);
        
    }

    render(){
        let input
        return (
            <div className="panel-header">
                 <form
                onSubmit={e => {
                e.preventDefault();
                if (!input.value.trim()) {
                    return
                };
                this.handlerAddToDo(input.value);
                input.value = ''
            }}>
                <input ref={node => {
                    input = node
                }}/>
                <button type="submit">
                    Add Todo
                </button>
            </form>
            </div>
        )
    }
}

export default TodoHeader;

TodoItem.js

import React from "react"
import ReactDom from 'react-dom'
export default class TodoItem extends React.Component{

    // 处理任务是否完成状态
    handlerChange(){
        let isDone = !this.props.isDone;
        this.props.changeTodoState(this.props.index, isDone);
    }


    // 删除当前任务
    handlerDelete(){
        this.props.deleteTodo(this.props.index);
    }

    render(){
        let doneStyle = this.props.isDone ? {textDecoration: 'line-through'} : {textDecoration: 'none'};

        return (
            <li>
                <input type="checkbox" checked={this.props.isDone} onChange={this.handlerChange.bind(this)}/>
                <span style={doneStyle}>{this.props.text}</span>
               
            </li>
        )
    }
}

TodoMain.js

import React from "react";
import TodoItem from "./TodoItem.js"


export default class TodoMain extends React.Component{
    // 遍历显示任务,转发props
    render(){
        return (
            <ul className="todo-list">
                {this.props.todos.map((todo, index) => {
                    return <TodoItem key={index} {...todo} index={index} {...this.props}/>
                })}
            </ul>
        )
    }
}

App.js

import React from "react";
import LocalDb from "localDb";

import TodoHeader from "./TodoHeader.js";
import TodoMain from "./TodoMain.js";
import TodoFooter from "./TodoFooter.js";

class App extends React.Component {
    constructor(){
        super();
        this.db = new LocalDb('React-Todos');
        this.state = {
            todos: this.db.get("todos") || [],
            isAllChecked: false
        };
    }

    // 判断是否所有任务的状态都完成,同步底部的全选框
    allChecked(){
        let isAllChecked = false;
        if(this.state.todos.every((todo)=> todo.isDone)){
            isAllChecked = true;
        }
        this.setState({todos: this.state.todos, isAllChecked});
    }

    // 添加任务,是传递给Header组件的方法
    addTodo(todoItem){
        this.state.todos.push(todoItem);
        this.allChecked();
        this.db.set('todos',this.state.todos);
    }

    // 改变任务状态,传递给TodoItem和Footer组件的方法
    changeTodoState(index, isDone, isChangeAll=false){
        if(isChangeAll){
            this.setState({
                todos: this.state.todos.map((todo) => {
                    todo.isDone = isDone;
                    return todo;
                }),
                isAllChecked: isDone
            })
        }else{
            this.setState({
                todos:this.state.todos[index].isDone=isDone
            })
            this.allChecked();
        }
        this.db.set('todos', this.state.todos);
    }

    // 清除已完成的任务,传递给Footer组件的方法
    clearDone(){
        let todos = this.state.todos.filter(todo => !todo.isDone);
        this.setState({
            todos: todos,
            isAllChecked: false
        });
        this.db.set('todos', todos);
    }

    // 删除当前的任务,传递给TodoItem的方法
    deleteTodo(index){
        this.state.todos.splice(index, 1);
        this.setState({todos: this.state.todos});
        this.db.set('todos', this.state.todos);
    }

    render(){
        var props = {
            todoCount: this.state.todos.length || 0,
            todoDoneCount: (this.state.todos && this.state.todos.filter((todo)=>todo.isDone)).length || 0
        };
        return (
            <div className="panel">
                <TodoHeader addTodo={this.addTodo.bind(this)}/>
                <TodoMain deleteTodo={this.deleteTodo.bind(this)} todos={this.state.todos} changeTodoState={this.changeTodoState.bind(this)}/>
                <TodoFooter isAllChecked={this.state.isAllChecked} clearDone={this.clearDone.bind(this)} {...props} changeTodoState={this.changeTodoState.bind(this)}/>
            </div>
        )
    }
}
export default App

很常规的写法

但有一些问题
  • UI和逻辑没有完全分离
  • APP.js中到处都是setState


使用redux后呢?


Add Todo功能

path:./actions/index.js

let nextTodoId = 0;
export const addTodo = (text) => ({
    type: 'ADD_TODO',
    id: nextTodoId++,
    text
})

path:./reducers/todos.js

const todos = (state = [], action) => {
    switch (action.type) {
        case 'ADD_TODO':
            return [
                ...state, {
                    id: action.id,
                    text: action.text,
                    completed: false
                }
            ]
            default:
            return state;
    }
}

export default todos;

path:./containers/AddTodo.js

import React from 'react'
import {connect} from 'react-redux'
import {addTodo} from '../actions'
//此处混用了component和container 给addTodo分配action:dispatch(addTodo(input.value));
let AddTodo = ({dispatch}) => {
    let input
    return (
        <div>
            <form
                onSubmit={e => {
                e.preventDefault();
                if (!input.value.trim()) {
                    return
                };
                dispatch(addTodo(input.value));
                input.value = ''
            }}>
                <input ref={node => {
                    input = node
                }}/>
                <button type="submit">
                    Add Todo
                </button>
            </form>
        </div>
    )
}

AddTodo = connect()(AddTodo)
export default AddTodo

toggle todo 功能

path:./actions/index.js

export const toggleTodo = (id) => ({type: 'TOGGLE_TODO', id})

path:./reducers/todos.js

const todos = (state = [], action) => {
    switch (action.type) {
        case 'TOGGLE_TODO':
            return state.map(todo => (todo.id === action.id)
                ? {
                    ...todo,
                    completed: !todo.completed
                }
                : todo)

        default:
            return state;
    }
}

export default todos;

path:./components/Todo.js

import React from 'react'
import PropTypes from 'prop-types'

const Todo = ({onClick, completed, text}) => (
    <li
       
        style={{
        textDecoration: completed
            ? 'line-through'
            : 'none'
    }}>
    <input type="checkbox" onClick={onClick} />
        {text}
    </li>
)

Todo.propTypes = {
    onClick: PropTypes.func.isRequired,
    completed: PropTypes.bool.isRequired,
    text: PropTypes.string.isRequired
}

export default Todo

path:./components/TodoList.js

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'

const TodoList = ({todos, onTodoClick}) => (
    <ul>
        {todos.map(todo => <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)}/>)}
    </ul>
)

TodoList.propTypes = {
    todos: PropTypes
        .arrayOf(PropTypes.shape({id: PropTypes.number.isRequired, completed: PropTypes.bool.isRequired, text: PropTypes.string.isRequired}).isRequired)
        .isRequired,
    onTodoClick: PropTypes.func.isRequired
}

export default TodoList

path:./containers/VisibleTodoList.js

import {connect} from 'react-redux'
import {toggleTodo} from '../actions'
import TodoList from '../components/TodoList'

const getVisibleTodos = (todos, filter) => {
    switch (filter) {
        case 'SHOW_ALL':
            return todos
        case 'SHOW_COMPLETED':
            return todos.filter(t => t.completed)
        case 'SHOW_ACTIVE':
            return todos.filter(t => !t.completed)
        default:
            throw new Error('Unknown filter: ' + filter)
    }
}

const mapStateToProps = (state) => ({
    todos: getVisibleTodos(state.todos, state.visibilityFilter)
})

const mapDispatchToProps = {
    onTodoClick: toggleTodo
}

const VisibleTodoList = connect(mapStateToProps, mapDispatchToProps)(TodoList)

export default VisibleTodoList

connect 是什么

  • 把 React 组件和 Redux 的 store 连接起来
connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

mapStateToProps

  • mapStateToProps(state, [ownProps]): stateProps: 如果定义该参数,组件将会监听 Redux store 的变化。任何时候,只要 Redux store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象会与组件的 props 合并。如果你省略了这个参数,你的组件将不会监听 Redux store

  • 通常在state变化后想改变props时使用

mapDispatchToProps

  • mapDispatchToProps(dispatch, [ownProps]): dispatchProps: 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作 Redux action creator,对象所定义的方法名将作为属性名;每个方法将返回一个新的函数,函数中dispatch方法会将action creator的返回值作为参数执行。这些属性会被合并到组件的 props 中

  • 通常在用户输入后(onClick等)需要通过props触发action时使用

mergeProps

  • 不管是 stateProps 还是 dispatchProps,都需要和 ownProps merge 之后才会被赋给 组件。connect 的第三个参数就是用来做这件事。通常情况下,你可以不传这个参数,connect 就会使用 Object.assign 替代该方法

  • 这样我们就把redux store和UI联系到一起了

Redux数据流


react-redux数据流


介绍一个好用的调试工具

ReduxDevTools


  • 清晰直观的看出每个action的触发和state的变化


  • 可以回放任意action


我犯过的一些错误

AddTodo = connect(AddTodo)
const FilterLink = connect(mapDispatchToProps)(Link)

正确的写法

AddTodo = connect()(AddTodo)
const FilterLink = connect(null,mapDispatchToProps)(Link)



错误引发的一点想法

  • 因为JavaScript没有类型检查,第一个错误在编译前完全无感知。是否可以引入TypeScript构建react&redux应用

相爱?


  • 并没有😂
  • 理解还不够深
  • 不能为了redux而redux,就像不能为了MVC而MVC
  • 每种写法都有它存在的道理,也有它的弊端,合适的才是最好的

Thank You

Comments
Write a Comment
  • 小粉丝x7 reply

    这么快就更新上来了,动作利索,我发现标题都是一样,难道上传上来不需要二次编辑?对博客文章上传的功能感兴趣。#倒数第二段#相爱没有那么容易;#倒数第一段#不客气。 [写在最后:我可能是全场唯一看不懂代码的真粉丝了]。

    • 图南 reply

      一直都在同步更新啊!你没发现而已啦,晚上跟你说说博客文章的上传机制,很好用的@小粉丝x7