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
看到了这样的代码
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
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)
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如何实现
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;
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>
)
}
}
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>
)
}
}
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功能
let nextTodoId = 0;
export const addTodo = (text) => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
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;
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 功能
export const toggleTodo = (id) => ({type: 'TOGGLE_TODO', id})
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;
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
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
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
- 每种写法都有它存在的道理,也有它的弊端,合适的才是最好的
这么快就更新上来了,动作利索,我发现标题都是一样,难道上传上来不需要二次编辑?对博客文章上传的功能感兴趣。#倒数第二段#相爱没有那么容易;#倒数第一段#不客气。 [写在最后:我可能是全场唯一看不懂代码的真粉丝了]。
一直都在同步更新啊!你没发现而已啦,晚上跟你说说博客文章的上传机制,很好用的@小粉丝x7