跳到内容

b

表单

让我们继续扩展我们的应用,允许用户添加新的笔记。你可以在这里找到我们当前应用的代码。

在组件状态中保存笔记

为了让我们的页面在添加新的笔记时得到更新,最好将笔记存储在App组件的状态中。让我们导入useState函数,用它来定义一个状态片段,并将其初始化为props传递的初始笔记数组。

import { useState } from 'react'import Note from './components/Note'

const App = (props) => {  const [notes, setNotes] = useState(props.notes)
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
    </div>
  )
}

export default App

组件使用useState函数来将存储在notes中的状态片段初始化为props传递的笔记数组:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)

  // ...
}

我们还可以用React开发者工具看到事实确实如此:

browser showing dev react tools window

如果我们想从一个空的笔记列表开始,我们会把初始值设置为一个空的数组,这样props就用不到了,于是我们可以从函数定义中省略props参数:

const App = () => {
  const [notes, setNotes] = useState([])

  // ...
}

让我们先继续使用props中传递的初始值。

接下来,让我们在组件中添加一个HTML表单,用来添加新的笔记。

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)

  const addNote = (event) => {    event.preventDefault()    console.log('button clicked', event.target)  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>        <input />        <button type="submit">save</button>      </form>    </div>
  )
}

我们将addNote函数作为事件处理函数添加到表单元素中,当点击提交按钮时,就会提交表单,同时调用该函数。

我们使用在第1章节中讨论的方法来定义我们的事件处理函数:

const addNote = (event) => {
  event.preventDefault()
  console.log('button clicked', event.target)
}

event参数是触发调用事件处理函数的事件

事件处理函数立即调用event.preventDefault()方法,防止提交表单的默认动作。默认动作会导致页面重新加载,以及其他一些事情

存储在event.target中的事件目标被记录到控制台。

fullstack content

本例中的目标是我们在组件中定义的表单。

那么我们如何访问表单的input元素中包含的数据呢?

受控组件

可以通过很多方法;我们要看的第一个方法是通过使用所谓的受控组件

让我们添加一个用来存储用户提交输入的新状态片段,叫做newNote然后让我们把它设为input元素的value属性:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(    'a new note...'  )
  const addNote = (event) => {
    event.preventDefault()
    console.log('button clicked', event.target)
  }

  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input value={newNote} />        <button type="submit">save</button>
      </form>
    </div>
  )
}

存储为newNote状态初始值的占位符文本出现在了input元素中,但输入框中的文本不能编辑。控制台显示了一个警告,提示了我们哪里可能出错:

fullstack content

因为我们将App组件的一个状态片段赋值给input元素的value属性,App组件现在控制了input元素的行为。

为了实现对输入框元素的编辑,我们必须注册一个事件处理函数来将输入框的发生变化同步到组件的状态:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState(
    'a new note...'
  )

  // ...

  const handleNoteChange = (event) => {    console.log(event.target.value)    setNewNote(event.target.value)  }
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notes.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleNoteChange}        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

我们现在已经为表单的input元素的onChange属性注册了一个事件处理函数:

<input
  value={newNote}
  onChange={handleNoteChange}
/>

每当input元素发生变化时都会调用事件处理函数。事件处理函数接收事件对象作为其event参数:

const handleNoteChange = (event) => {
  console.log(event.target.value)
  setNewNote(event.target.value)
}

事件对象的target属性现在对应于被控制的input元素,而event.target.value指的是该元素的输入值。

注意我们不需要像在onSubmit的事件处理函数中那样调用event.preventDefault()方法。这是因为在输入框变化时没有默认动作,这与提交表单时不同。

你可以盯着控制台,看看事件处理函数是如何被调用的:

fullstack content

记得安装React devtools了吧?很好。你可以直接在React Devtools标签页中查看状态是如何变化的。

fullstack content

现在App组件的newNote状态反映了输入框的当前值,这意味着我们可以完成创建新笔记的addNote功能了:

const addNote = (event) => {
  event.preventDefault()
  const noteObject = {
    content: newNote,
    important: Math.random() < 0.5,
    id: String(notes.length + 1),
  }

  setNotes(notes.concat(noteObject))
  setNewNote('')
}

首先,我们为笔记创建一个新对象noteObject,它将从组件的newNote状态接收其内容。唯一标识符id是根据笔记的总数生成的。这种方法适用于我们的应用,因为笔记永远不会被删除。在Math.random()函数的帮助下,我们的笔记有50%的几率被标记为重要的。

新笔记通过第1章节中介绍过的concat数组方法添加到笔记列表中:

setNotes(notes.concat(noteObject))

该方法并不改变原始的notes数组,而是创建一个新的数组副本,将新项添加到最后。这很重要,因为在React中我们必须永远不要直接改变状态

事件处理函数也通过调用newNote状态的setNewNote函数来重置受控input元素的值:

setNewNote('')

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

筛选展示的元素

让我们为我们的应用添加一些新功能,让我们可以选择只查看重要的笔记。

让我们向App组件中添加一个状态片段来跟踪哪些笔记应该被显示:

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)
  // ...
}

让我们改变这个组件,让它在notesToShow变量中存储一个要显示的所有笔记的列表。列表的内容取决于组件的状态:

import { useState } from 'react'
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  const notesToShow = showAll    ? notes    : notes.filter(note => note.important === true)
  return (
    <div>
      <h1>Notes</h1>
      <ul>
        {notesToShow.map(note =>          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...
    </div>
  )
}

notesToShow变量的定义相当紧凑:

const notesToShow = showAll
  ? notes
  : notes.filter(note => note.important === true)

该定义使用了条件运算符,在许多其他编程语言中也有。

该运算符的功能如下。如果我们有:

const result = condition ? val1 : val2

如果condition为true,result变量将被设为val1的值。如果condition为false,result变量将被设为val2的值。

如果showAll的值为false,notesToShow变量将被赋值为一个只包含important属性为true的笔记的列表。筛选的过程是借助数组方法filter完成的:

notes.filter(note => note.important === true)

比较运算符实际上是多余的,因为note.important的值不是true就是false,这意味着我们可以简写成:

notes.filter(note => note.important)

我们之所以先展示比较运算符,是为了强调一个重要的细节:在JavaScript中,val1 == val2并不是在所有情况下都与预期一样。因此,只用val1 === val2进行比较会更安全。你可以在这里阅读更多关于这个主题的内容。

你可以通过改变showAll状态的初始值来测试筛选的功能。

接下来,让我们添加让用户能够从用户界面上切换应用的showAll状态的功能。

相关的改变如下:

import { useState } from 'react'
import Note from './components/Note'

const App = (props) => {
  const [notes, setNotes] = useState(props.notes)
  const [newNote, setNewNote] = useState('')
  const [showAll, setShowAll] = useState(true)

  // ...

  return (
    <div>
      <h1>Notes</h1>
      <div>        <button onClick={() => setShowAll(!showAll)}>          show {showAll ? 'important' : 'all' }        </button>      </div>      <ul>
        {notesToShow.map(note =>
          <Note key={note.id} note={note} />
        )}
      </ul>
      // ...
    </div>
  )
}

显示的笔记(所有的或者重要的)是用一个按钮控制的。这个按钮的事件处理函数非常简单,所以就直接在按钮元素的属性中定义了。该事件处理函数将showAll的值从true切换到false,或者反过来,从false切换回true。

() => setShowAll(!showAll)

按钮的文本取决于showAll状态的值:

show {showAll ? 'important' : 'all'}

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