Redux 例子分析: 从 UI 角度解析 todos

Redux 例子分析: todos 中,我从代码的角度来说明 todos 这个例子是怎样实现的。如果很了解redux,一目了然,但是如果不太熟悉,那么就觉得有些难度了。如果能从UI的角度来逐步分析,应该会更加直接。
运行 todos,做出如下效果:
效果图

实现Todo 列表

单条记录 Todo

先来分析一下 list 中的这两项,即 hello 和 goodbye。想要在浏览器里看到他们,对应的HTML要写成如下形式:

<li style="text-decoration: none;">hello</li>
<li style="text-decoration: line-through;">goodbye</li>

对于每一条记录,记作一个todo,把它写成一个 react 的 component:

const Todo = () => (
    <li style="text-decoration: none;">hello</li>
)

const Todo = () => (
    <li style="text-decoration: line-through;">goodbye</li>
)

可以看到,需要两个变量,一个表示text-decoration,另一个表示 hello 或者 goodbye。这个 component 可以写成:

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

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

为了能在 ‘line-through’ 和 ‘none’ 之间切换,Todo 还需要它的另外一个属性 onClick

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

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

记录列表 TodoList

如此,我们就得到我们想要的表示一条 todo 记录的 component。
接下来,todo 的列表如何生成?我想要的是这样的。

<ul>
    <li .../>
    <li .../>
    <li .../>
    <li .../>
</ul>

因为每个 <li .../>对应一个 todo,那么,上面这个等价于:

<ul>
    <Todo key=... onClick=... completed=... text=.../>
    <Todo key=... onClick=... completed=... text=.../>
    <Todo key=... onClick=... completed=... text=.../>
    <Todo key=... onClick=... completed=... text=.../>
</ul>

这里的keyonClickcompletedtext都可以从外部输入。每一条记录的onClick的功能是相同的,所以可以使用同一个函数。
为每一条记录准备的数据的结构可以如下:

{
    id: PropTypes.number.isRequired, // key
    completed: PropTypes.bool.isRequired, // completed
    text: PropTypes.string.isRequired // text
}
onTodoClick: PropTypes.func.isRequired // onClick

对于多条记录,可以转换成如下形式:

todos: arrayOf({
    id: PropTypes.number.isRequired, // key
    completed: PropTypes.bool.isRequired, // completed
    text: PropTypes.string.isRequired // text
})
onTodoClick: PropTypes.func.isRequired // onClick

那么,如果已经有了输入数据,即有了 todos 和 onTodoClick,那么:

<ul>
    <Todo key=... onClick=... completed=... text=.../>
    <Todo key=... onClick=... completed=... text=.../>
    <Todo key=... onClick=... completed=... text=.../>
    <Todo key=... onClick=... completed=... text=.../>
</ul>

可以转换成:

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

所以,表示todo 列表的 component 如下,它需要 todosonTodoClick 两个属性:

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
}

到目前为止,这些都是关于 React 的。要想和 Redux 结合在一起,需要引入 react-redux 中的connect 模块,它的用法:

import { connect } from 'react-redux'

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

TodoList 与 redux-react 结合 - VisibleTodoList

通俗来说,就是通过mapStateToPropsmapDispatchToProps,把当前的 component TodoList 和 redux 连接起来。
引用 Redux 入门教程(三):React-Redux 的用法

mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。

// 容器组件的代码
//    <FilterLink filter="SHOW_ALL">
//      All
//    </FilterLink>

const mapStateToProps = (state, ownProps) => {
  return {
    active: ownProps.filter === state.visibilityFilter
  }
}

mapDispatchToProps是connect函数的第二个参数,用来建立 UI 组件的参数到store.dispatch方法的映射。也就是说,它定义了哪些用户的操作应该当作 Action,传给 Store。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。

const mapDispatchToProps = (
  dispatch,
  ownProps
) => {
  return {
    onClick: () => {
      dispatch({
        type: 'SET_VISIBILITY_FILTER',
        filter: ownProps.filter
      });
    }
  };
}

从上面代码可以看到,mapDispatchToProps作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了 UI 组件的参数怎样发出 Action。
如果mapDispatchToProps是一个对象,它的每个键名也是对应 UI 组件的同名参数,键值应该是一个函数,会被当作 Action creator ,返回的 Action 会由 Redux 自动发出。举例来说,上面的mapDispatchToProps写成对象就是下面这样。

const mapDispatchToProps = {
  onClick: (filter) => {
    type: 'SET_VISIBILITY_FILTER',
    filter: filter
  };
}

假设 Redux 已经帮我设定好了所有 state。格式: var state = { todos: [], visibilityFilter: '' }
一个 todos 中的元素,格式:{id: '', completed: '', text: ''}
visibilityFilter是一个字符串,它有三个有效值:
‘SHOW_ALL’,’SHOW_COMPLETED’,’SHOW_ACTIVE’。
不同的值,可以对 todos 做不同类型的过滤:

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)
  }
}

我想通过 TodoList 来构造一个VisibleTodoList,而TodoList需要两个 属性:{ todos, onTodoClick }
通过mapStateToProps得到属性todos

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

通过mapDispatchToProps得到属性onTodoClick

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

const mapDispatchToProps = {
  onTodoClick: toggleTodo
}

如此,即可得到 VisibleTodoList

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)

ToggleTode 对应的 Action & Reducer

简单来说,在 Redux 中,用户定义一个 Action,然后一个对应的 Reducer。当调用 store.dispatch(action),对应的 Reducer 就会工作。
上面的 Action 已经定义好了:

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

对应的 Reducer:

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
  }
}

要想将上面两个联系起来,还需要创建一个 store

const store = createStore(reducer)

可以直接将上面的 Reducer,即 todos代替下面创建storereducer。但是,通常情况下,我们并不会只有一个reducer,而且从代码中看到,他的 state 也只是处理 todo 列表。所以我们肯定需要多个reducer。即:

combineReducers({
  todos,
  ...//其它 reducer
})

我们并没有给 todos对应的名称,所有在 JS 中,上面等价于:

combineReducers({
  todos: todos,
  ...//其它 reducer
})

所以,可以通过 state.todos 来访问它。

实现脚注 Show: All, Active, Completed

如果想要得到文章开头途中的脚注,需要的HTML应该是:

<p>
    Show:
    <span>All</span>
    , 
    <a href="#">Active</a>
    , 
    <a href="#">Completed</a>
</p>

显然,每一条记录都可以用一个 component 来表示:

// active 表示是否激活,用来决定是用<span>还是<a>
// children 即sub component,在这里是文字
// onClick 表示如何响应
const Link = ({ active, children, onClick }) => {
  if (active) {
    return <span>{children}</span>
  }

  return (
    // eslint-disable-next-line
    <a href="#"
       onClick={e => {
         e.preventDefault()
         onClick()
       }}
    >
      {children}
    </a>
  )
}

Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}

我们需要从 Redux 获得 Link 所需的 activeonClick

// 获取active
const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})
// 获取 onClick
const setVisibilityFilter = (filter) => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})
const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => {
    dispatch(setVisibilityFilter(ownProps.filter))
  }
})

Action:

const setVisibilityFilter = (filter) => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})

Reducer:

const visibilityFilter = (state = 'SHOW_ALL', action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

和前面的 Reducer 合并:

combineReducers({
  todos,
  visibilityFilter
})

可以通过storestate.visibilityFilter来访问属性。

增加记录

对应的HTML

<div>
    <form>
        <input>
        <button type="submit">Add Todo</button>
    </form>
</div>

AddTodo

在上面用connect 的时候,我们都传入了mapStateToPropsmapDispatchToProps作为参数,如果什么也不传入,还有一种形式可以使用 Redux 中的功能,直接在属性中写 dispatch

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)

对应的Action 和 Reducer

Action addTodo:

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

Reducer:

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

与 VisibleTodoList 的 Reducer 合并:

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

如此,即可得到想要的效果。本来仅仅设计代码逻辑,代码如何组织请参考源代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值