跳到内容

e

到目前为止,我们在react-redux的hook-api的帮助下使用我们的redux-store。

实际上这意味着使用useSelectoruseDispatch函数。

为了完成这一部分,我们将研究另一种更古老、更复杂的使用redux的方式,即 react-redux 提供的 connect 函数。

In new applications you should absolutely use the hook-api, but knowing how to use connect is useful when maintaining older projects using redux.

Using the connect-function to share the redux store to components

让我们修改Notes组件,使其不再使用hook-api(useDispatchuseSelector 函数)而是使用 connect 函数。

我们必须修改该组件的以下部分。

import { useDispatch, useSelector } from 'react-redux'import { toggleImportanceOf } from '../reducers/noteReducer'

const Notes = () => {
  const dispatch = useDispatch()  const notes = useSelector(({filter, notes}) => {    if ( filter === 'ALL' ) {      return notes    }    return filter === 'IMPORTANT'      ? notes.filter(note => note.important)      : notes.filter(note => !note.important)  })
  return(
    <ul>
      {notes.map(note =>
        <Note
          key={note.id}
          note={note}
          handleClick={() =>
            dispatch(toggleImportanceOf(note.id))          }
        />
      )}
    </ul>
  )
}

export default Notes

connect 函数可以用来转换 "常规 "的React组件,这样Redux商店的状态就可以 "映射 "到组件的props中。

让我们首先使用connect函数将我们的Notes组件转换成connected component

import { connect } from 'react-redux'import { toggleImportanceOf } from '../reducers/noteReducer'

const Notes = () => {
  // ...
}

const ConnectedNotes = connect()(Notes)export default ConnectedNotes

该模块导出了连接组件,其工作原理与之前的普通组件完全相同。

该组件需要Redux商店中的笔记列表和过滤器的值。connect函数接受一个所谓的mapStateToProps函数作为其第一个参数。该函数可用于定义连接组件的prop,这些prop是基于Redux商店的状态。

如果我们定义。

const Notes = (props) => {  const dispatch = useDispatch()

  const notesToShow = () => {    if ( props.filter === 'ALL' ) {      return props.notes    }    return props.filter  === 'IMPORTANT'      ? props.notes.filter(note => note.important)      : props.notes.filter(note => !note.important)  }
  return(
    <ul>
      {notesToShow().map(note =>        <Note
          key={note.id}
          note={note}
          handleClick={() =>
            dispatch(toggleImportanceOf(note.id))
          }
        />
      )}
    </ul>
  )
}

const mapStateToProps = (state) => {
  return {
    notes: state.notes,
    filter: state.filter,
  }
}

const ConnectedNotes = connect(mapStateToProps)(Notes)
export default ConnectedNotes

Notes组件可以直接访问商店的状态,例如,通过包含笔记列表的props.notes。 类似地,props.filter引用过滤器的值。

使用connect和我们定义的mapStateToProps函数所产生的情况可以这样可视化。

fullstack content

Notes组件可以通过props.notesprops.filter"直接访问 "Redux商店的状态。

NoteList组件实际上不需要关于哪个过滤器被选中的信息,所以我们可以将过滤逻辑移到其他地方。

我们只需要在notesprop中给它正确的过滤的笔记。

const Notes = (props) => {
  const dispatch = useDispatch()

  return(
    <ul>
      {props.notes.map(note =>
        <Note
          key={note.id}
          note={note}
          handleClick={() =>
            dispatch(toggleImportanceOf(note.id))
          }
        />
      )}
    </ul>
  )
}

const mapStateToProps = (state) => {  if ( state.filter === 'ALL' ) {    return {      notes: state.notes    }  }  return {    notes: (state.filter  === 'IMPORTANT'      ? state.notes.filter(note => note.important)      : state.notes.filter(note => !note.important)    )  }}
const ConnectedNotes = connect(mapStateToProps)(Notes)
export default ConnectedNotes

mapDispatchToProps

现在我们已经摆脱了useSelector,但是Notes仍然使用useDispatch钩子和返回它的dispatch函数。

const Notes = (props) => {
  const dispatch = useDispatch()
  return(
    <ul>
      {props.notes.map(note =>
        <Note
          key={note.id}
          note={note}
          handleClick={() =>
            dispatch(toggleImportanceOf(note.id))          }
        />
      )}
    </ul>
  )
}

connect函数的第二个参数可用于定义mapDispatchToProps,这是一组action creator函数,作为props传递给连接的组件。让我们对我们现有的连接操作做如下修改。

const mapStateToProps = (state) => {
  return {
    notes: state.notes,
    filter: state.filter,
  }
}

const mapDispatchToProps = {  toggleImportanceOf,}
const ConnectedNotes = connect(
  mapStateToProps,
  mapDispatchToProps)(Notes)

export default ConnectedNotes

现在,组件可以直接调度由toggleImportanceOf动作创建者定义的动作,通过其props调用该函数。

const Notes = (props) => {
  return(
    <ul>
      {props.notes.map(note =>
        <Note
          key={note.id}
          note={note}
          handleClick={() => props.toggleImportanceOf(note.id)}
        />
      )}
    </ul>
  )
}

这意味着,不再像这样调度动作了。

dispatch(toggleImportanceOf(note.id))

当使用connect时,我们可以简单地这样做。

props.toggleImportanceOf(note.id)

没有必要单独调用dispatch函数,因为connect已经将toggleImportanceOf动作创建者修改为包含调度的形式。

你可能需要一些时间来理解mapDispatchToProps的工作原理,尤其是当我们看了一个使用它的替代方法

使用connect所产生的情况可以被可视化为这样。

fullstack content

除了通过props.notesprops.filter访问商店的状态外,该组件还引用了一个函数,该函数可用于通过其toggleImportanceOfprop调度notes/toggleImportanceOf型动作。

新重构的Notes组件的代码如下所示:

import { connect } from 'react-redux'
import { toggleImportanceOf } from '../reducers/noteReducer'

const Notes = (props) => {
  return(
    <ul>
      {props.notes.map(note =>
        <Note
          key={note.id}
          note={note}
          handleClick={() => props.toggleImportanceOf(note.id)}
        />
      )}
    </ul>
  )
}

const mapStateToProps = (state) => {
  if ( state.filter === 'ALL' ) {
    return {
      notes: state.notes
    }
  }

  return {
    notes: (state.filter  === 'IMPORTANT'
    ? state.notes.filter(note => note.important)
    : state.notes.filter(note => !note.important)
    )
  }
}

const mapDispatchToProps = {
  toggleImportanceOf
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notes)

让我们也使用connect来创建新的笔记。

import { connect } from 'react-redux'
import { createNote } from '../reducers/noteReducer'

const NewNote = (props) => {
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    props.createNote(content)  }

  return (
    <form onSubmit={addNote}>
      <input name="note" />
      <button type="submit">add</button>
    </form>
  )
}

export default connect(  null,  { createNote })(NewNote)

由于该组件不需要访问商店的状态,我们可以简单地传递null作为connect的第一个参数。

你可以在这个Github仓库part6-5分支中找到我们当前应用的全部代码。

Referencing action creators passed as props

让我们注意一下NewNote组件中一个有趣的细节。

import { connect } from 'react-redux'
import { createNote } from '../reducers/noteReducer'
const NewNote = (props) => {

  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    props.createNote(content)  }

  return (
    <form onSubmit={addNote}>
      <input name="note" />
      <button type="submit">add</button>
    </form>
  )
}

export default connect(
  null,
  { createNote })(NewNote)

初次接触连接的开发者可能会发现,组件中的createNote动作创建者有两个版本,这令人费解。

该函数必须通过组件的props被引用为props.createNote,因为这是包含connect添加的自动调度的那个版本。

由于动作创建者的导入方式。

import { createNote } from './../reducers/noteReducer'

也可以通过调用createNote直接引用动作创建者。你不应该这样做,因为这是未经修改的动作创建者的版本,不包含添加的自动调度。

如果我们从代码中把这些函数打印到控制台(我们还没有看这个有用的调试技巧)。

const NewNote = (props) => {
  console.log(createNote)
  console.log(props.createNote)

  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    props.createNote(content)
  }

  // ...
}

我们可以看到这两个函数之间的区别。

fullstack content

第一个函数是一个普通的动作创建者,而第二个函数则包含了由connect添加的对存储的额外调度。

Connect是一个非常有用的工具,尽管由于它的抽象程度,一开始可能看起来很困难。

Alternative way of using mapDispatchToProps

我们以如下方式定义了用于从连接的NewNote组件中调度动作的函数。

const NewNote = () => {
  // ...
}

export default connect(
  null,
  { createNote }
)(NewNote)

上面的连接表达式使该组件能够通过props.createNote("a new note")命令来调度创建新笔记的动作。

mapDispatchToProps中传递的函数必须是action creators,也就是返回Redux动作的函数。

值得注意的是,mapDispatchToProps参数是一个JavaScript对象,正如其定义。

{
  createNote
}

这只是定义对象字面的速记。

{
  createNote: createNote
}

这是一个具有单一的createNote属性的对象,其值为createNote函数。

另外,我们可以把下面的函数定义作为第二个参数传递给 connect

const NewNote = (props) => {
  // ...
}

const mapDispatchToProps = dispatch => {  return {    createNote: value => {      dispatch(createNote(value))    },  }}
export default connect(
  null,
  mapDispatchToProps
)(NewNote)

在这个替代定义中,mapDispatchToProps是一个函数,connect将把dispatch-函数作为其参数传递给它,从而调用该函数。该函数的返回值是一个定义了一组函数的对象,这些函数被作为prop传递给连接的组件。我们的例子定义了作为createNoteprop传递的函数。

value => {
  dispatch(createNote(value))
}

它简单地分配了用createNote动作创建器创建的动作。

然后组件通过它的prop调用props.createNote来引用这个函数。

const NewNote = (props) => {
  const addNote = (event) => {
    event.preventDefault()
    const content = event.target.note.value
    event.target.note.value = ''
    props.createNote(content)
  }

  return (
    <form onSubmit={addNote}>
      <input name="note" />
      <button type="submit">add</button>
    </form>
  )
}

这个概念相当复杂,通过文字来描述它是有难度的。在大多数情况下,使用mapDispatchToProps的较简单形式就足够了。然而,在某些情况下,更复杂的定义是必要的,例如,如果dispatched actions需要引用组件的prop

Redux的创造者Dan Abramov创造了一个精彩的教程,叫做Redux入门,你可以在Egghead.io上找到。我向大家强烈推荐这个教程。最后四个视频讨论了connect方法,特别是使用它的更 "复杂 "的方式。

Presentational/Container revisited

重构后的Notes组件几乎完全专注于渲染笔记,而且相当接近于所谓的展示性组件。根据Dan Abramov提供的描述,演示组件。

  • 关注事物的外观。

  • 里面可能同时包含展示组件和容器组件,并且通常有一些DOM标记和它们自己的样式。

  • 通常允许通过props.children进行包含。

  • 对应用的其他部分没有依赖性,例如Redux动作或商店。

  • 不指定数据是如何被加载或改变的。

  • 完全通过props接收数据和回调。

  • 很少有自己的状态(如果有,也是UI状态而不是数据)。

  • 除非它们需要状态、生命周期钩子或性能优化,否则就被写成功能组件。

connect函数创建的connected组件

const mapStateToProps = (state) => {
  if ( state.filter === 'ALL' ) {
    return {
      notes: state.notes
    }
  }

  return {
    notes: (state.filter  === 'IMPORTANT'
    ? state.notes.filter(note => note.important)
    : state.notes.filter(note => !note.important)
    )
  }
}

const mapDispatchToProps = {
  toggleImportanceOf,
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Notes)

符合container组件的描述。根据Dan Abramov提供的描述,容器组件。

  • 关注事物的工作方式。

  • 里面可能同时包含展示性组件和容器组件,但除了一些包裹性的div,通常没有任何自己的DOM标记,也没有任何样式。

  • 为展示性或其他容器组件提供数据和行为。

  • 调用Redux动作,并将其作为回调提供给渲染组件。

  • 通常是有状态的,因为它们往往作为数据源。

  • 通常使用高阶组件生成,如来自React Redux的connect,而不是手工编写。

将应用分为展示性组件和容器组件是一种被认为是有益的React应用结构的方式。这种划分可能是一个好的设计选择,也可能不是,这取决于环境。

Abramov将以下好处归于这种划分。

  • 更好地分离关注点。通过这样写组件,你可以更好地理解你的应用和你的用户界面。

  • 更好的重用性。你可以在完全不同的状态源中使用相同的表现型组件,并将这些组件变成可以进一步重用的独立容器组件。

  • 渲染式组件本质上是你的应用的 "调色板"。你可以把它们放在一个页面上,让设计者调整它们的所有变化,而不触及应用的逻辑。你可以在该页面上运行屏幕截图回归测试。

阿布拉莫夫提到了术语高阶组件Notes组件是一个普通组件的例子,而React-Redux提供的connect方法是一个高阶组件的例子。从本质上讲,高阶组件是一个接受 "常规 "组件作为其参数的函数,然后返回一个新的 "常规 "组件作为其返回值。

高阶组件,或称HOC,是一种定义可以应用于组件的通用功能的方式。这是一个来自函数式编程的概念,与面向对象编程中的继承非常相似。

HOCs实际上是高阶函数 (HOF)概念的概括。HOFs是接受函数作为参数或返回函数的函数。实际上,我们在整个课程中一直在使用HOF,例如,所有用于处理数组的方法,如map、filter和find都是HOF。

在React hook-api发布后,HOCs变得越来越不流行了。几乎所有曾经基于HOCs的库现在都被修改为使用钩子。大多数时候,基于钩子的api比基于HOC的api要简单得多,redux也是如此。

Redux and the component state

在这个课程中,我们已经走了很长的路,最后,我们已经走到了 "正确的方式 "使用React的地步,这意味着React只专注于生成视图,应用状态完全与React组件分离,并传递给Redux、其动作和其还原器。

那么useState-hook呢,它为组件提供了它们自己的状态?如果一个应用使用Redux或其他外部状态管理解决方案,它是否有任何作用?如果应用有更复杂的表单,使用useState函数提供的状态来实现它们的本地状态可能是有益的。当然,人们可以让Redux管理表单的状态,然而,如果表单的状态只在填写表单时才相关(例如用于验证),那么将状态的管理留给负责表单的组件可能是明智的。

我们应该总是使用redux吗?也许不是。redux的开发者Dan Abramov在他的文章You Might Not Need Redux中讨论了这个问题。

现在可以通过使用React context-api和useReducer-hook来实现类似redux的状态管理,而无需redux。

关于这个的更多信息这里这里。我们还将在以下方面进行实践

第9章节