跳到内容

b

props.children 与 proptypes

Displaying the login form only when appropriate

让我们修改应用,使其默认不显示登录表单。

fullstack content

当用户按下login按钮时,登录表单就会出现。

fullstack content

用户可以通过点击取消按钮来关闭登录表格。

我们先把登录表单提取到它自己的组件中。

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
  return (
    <div>
      <h2>Login</h2>

      <form onSubmit={handleSubmit}>
        <div>
          username
          <input
            value={username}
            onChange={handleUsernameChange}
          />
        </div>
        <div>
          password
          <input
            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
      </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

export default LoginForm

状态和所有与之相关的功能都是在组件之外定义的,并作为prop传递给组件。

注意,props是通过destructuring分配给变量的,这意味着不用再写。

const LoginForm = (props) => {
  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={props.handleSubmit}>
        <div>
          username
          <input
            value={props.username}
            onChange={props.handleChange}
            name="username"
          />
        </div>
        // ...
        <button type="submit">login</button>
      </form>
    </div>
  )
}

通过例如props.handleSubmit来访问props对象的属性,而是直接将属性分配给它们自己的变量。

实现该功能的一个快速方法是像这样改变App组件的loginForm函数。

const App = () => {
  const [loginVisible, setLoginVisible] = useState(false)
  // ...

  const loginForm = () => {
    const hideWhenVisible = { display: loginVisible ? 'none' : '' }
    const showWhenVisible = { display: loginVisible ? '' : 'none' }

    return (
      <div>
        <div style={hideWhenVisible}>
          <button onClick={() => setLoginVisible(true)}>log in</button>
        </div>
        <div style={showWhenVisible}>
          <LoginForm
            username={username}
            password={password}
            handleUsernameChange={({ target }) => setUsername(target.value)}
            handlePasswordChange={({ target }) => setPassword(target.value)}
            handleSubmit={handleLogin}
          />
          <button onClick={() => setLoginVisible(false)}>cancel</button>
        </div>
      </div>
    )
  }

  // ...
}

App组件的状态现在包含布尔值loginVisible,它定义了登录表单是否应该显示给用户。

loginVisible的值是通过两个按钮来切换的。这两个按钮的事件处理程序都直接定义在组件中。

<button onClick={() => setLoginVisible(true)}>log in</button>

<button onClick={() => setLoginVisible(false)}>cancel</button>

组件的可见性是通过给组件一个inline样式规则来定义的,其中display属性的值是none如果我们不希望组件被显示。

const hideWhenVisible = { display: loginVisible ? 'none' : '' }
const showWhenVisible = { display: loginVisible ? '' : 'none' }

<div style={hideWhenVisible}>
  // button
</div>

<div style={showWhenVisible}>
  // button
</div>

我们又一次使用了 "问号 "三元运算符。如果loginVisibletrue,那么该组件的CSS规则将是。

display: 'none';

如果loginVisiblefalse,那么display将不会收到与该组件的可见性有关的任何值。

The components children, aka. props.children

与管理登录表单的可见性有关的代码可以被认为是它自己的逻辑实体,由于这个原因,最好把它从App组件中提取到它自己的独立组件中。

我们的目标是实现一个新的Togglable组件,可以按以下方式使用。

<Togglable buttonLabel='login'>
  <LoginForm
    username={username}
    password={password}
    handleUsernameChange={({ target }) => setUsername(target.value)}
    handlePasswordChange={({ target }) => setPassword(target.value)}
    handleSubmit={handleLogin}
  />
</Togglable>

该组件的使用方式与我们以前的组件略有不同。该组件有开头和结尾标签,围绕着一个LoginForm组件。在React术语中,LoginFormTogglable的一个子组件。

我们可以在Togglable的开头和结尾标签之间添加任何我们想要的React元素,比如说这样。

<Togglable buttonLabel="reveal">
  <p>this line is at start hidden</p>
  <p>also this is hidden</p>
</Togglable>

Togglable组件的代码如下所示。

import { useState } from 'react'

const Togglable = (props) => {
  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
}

export default Togglable

代码中新的和有趣的部分是props.children,那是用来引用组件的子组件。子组件是我们在组件的打开和关闭标签之间定义的React元素。

这一次,子组件是在用于渲染组件本身的代码中被渲染出来的。

<div style={showWhenVisible}>
  {props.children}
  <button onClick={toggleVisibility}>cancel</button>
</div>

与我们之前看到的 "普通 "prop不同,children是由React自动添加的,并且一直存在。如果一个组件被定义了一个自动关闭的/>标签,像这样。

<Note
  key={note.id}
  note={note}
  toggleImportance={() => toggleImportanceOf(note.id)}
/>

那么props.children就是一个空数组。

Togglable组件是可重复使用的,我们可以用它来给用于创建新笔记的表单添加类似的可见性切换功能。

在我们这样做之前,让我们把创建笔记的表单提取到自己的组件中。

const NoteForm = ({ onSubmit, handleChange, value}) => {
  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={onSubmit}>
        <input
          value={value}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

接下来让我们在一个Togglable组件中定义这个表单组件。

<Togglable buttonLabel="new note">
  <NoteForm
    onSubmit={addNote}
    value={newNote}
    handleChange={handleNoteChange}
  />
</Togglable>

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

State of the forms

目前应用的状态在App组件中。

React 文档对放置状态的位置进行了如下说明:

有时,您希望两个组件的状态始终一起更改。要做到这一点,请从它们中删除状态,将其移动到它们最近的公共父级,然后通过 props 将其传递给它们。这被称为提升状态,这是你编写 React 代码时最常做的事情之一。

如果我们考虑到表单的状态,例如一个新的笔记在创建之前的内容,App组件实际上并不需要它。

我们也可以把表单的状态移到相应的组件上。

一个笔记的组件是这样变化的:

import { useState } from 'react'

const NoteForm = ({ createNote }) => {
  const [newNote, setNewNote] = useState('')

  const handleChange = (event) => {
    setNewNote(event.target.value)
  }

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: Math.random() > 0.5,
    })

    setNewNote('')
  }

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

export default NoteForm

注意 同时,我们改变了应用的行为,使得新的笔记默认为重要,也就是说,important 字段获得的值为 true

newNote 状态变量和负责改变它的事件处理器已经从 App 组件移动到负责笔记表单的组件。

现在只剩下一个 prop,即 createNote 函数,当创建新的笔记时,表单会调用它。

App 组件现在变得更简单,因为我们已经摆脱了 newNote 状态和它的事件处理器。 创建新笔记的 addNote 函数接收一个新的笔记作为参数,函数是我们发送给表单的唯一 prop:

const App = () => {
  // ...
  const addNote = (noteObject) => {
    noteService
      .create(noteObject)
      .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
  const noteForm = () => (
    <Togglable buttonLabel='new note'>
      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

我们可以对登录表单做同样的事情,但我们将把这留作可选的练习。

应用程序代码可以在 GitHub 上找到,分支为 part5-5

References to components with ref

我们目前的实现相当不错,但它有一个可以改进的方面。

创建新的笔记后,隐藏新的笔记表单是有意义的。目前,表单仍然可见。隐藏表单有一个小问题。可见性是由 Togglable 组件内部的 visible 状态变量控制的。

解决这个问题的一个办法是将 Togglable 组件的状态控制移出组件。然而,我们现在不会这样做,因为我们希望组件负责自己的状态。所以我们必须找到另一种解决方案,并找到一种机制来从外部改变组件的状态。

有几种不同的方法可以实现从组件外部访问组件的函数,但让我们使用 React 的 ref 机制,它提供了对组件的引用。

让我们对App组件做如下修改。

import { useState, useEffect, useRef } from 'react'
const App = () => {
  // ...
  const noteFormRef = useRef()
  const noteForm = () => (
    <Togglable buttonLabel='new note' ref={noteFormRef}>      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

useRef 钩子被用来创建一个noteFormRef参考,它被分配给包含创建笔记表单的Togglable组件。noteFormRef变量作为该组件的引用。这个钩子确保了在组件的重新渲染过程中保持相同的引用(ref)。

我们还对Togglable组件做了如下修改。

import { useState, forwardRef, useImperativeHandle } from 'react'
const Togglable = forwardRef((props, ref) => {  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  useImperativeHandle(ref, () => {    return {      toggleVisibility    }  })
  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
})
export default Togglable

创建该组件的函数被包裹在一个forwardRef函数调用中。这样,组件就可以访问分配给它的Ref。

该组件使用useImperativeHandle钩子来使它的toggleVisibility函数在组件之外可用。

我们现在可以在创建一个新的笔记后,通过调用noteFormRef.current.toggleVisibility()来隐藏这个表单。

const App = () => {
  // ...
  const addNote = (noteObject) => {
    noteFormRef.current.toggleVisibility()    noteService
      .create(noteObject)
      .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
}

回顾一下,useImperativeHandle函数是一个React钩子,用于在组件中定义可以从组件外部调用的函数。

这个技巧对于改变组件的状态是有效的,但它看起来有点不爽。我们可以使用 "老式React "基于类的组件,用稍微干净的代码完成同样的功能。我们将在教材的第7章节看一下这些类组件。到目前为止,这是唯一一种使用React钩子导致的代码不比使用类组件干净的情况。

除了访问React组件,还有其他用例的参考文献。

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

One point about components

当我们在React中定义一个组件。

const Togglable = () => ...
  // ...
}

然后像这样使用它。

<div>
  <Togglable buttonLabel="1" ref={togglable1}>
    first
  </Togglable>

  <Togglable buttonLabel="2" ref={togglable2}>
    second
  </Togglable>

  <Togglable buttonLabel="3" ref={togglable3}>
    third
  </Togglable>
</div>

我们创建了该组件的三个独立实例,它们都有自己的独立状态。

fullstack content

ref属性用于为变量togglable1togglable2togglable3中的每个组件分配一个引用。

PropTypes

Togglable组件假定它通过buttonLabelprop得到了按钮的文本。如果我们忘记向组件定义它。

<Togglable> buttonLabel forgotten... </Togglable>

应用可以工作,但浏览器显示的按钮没有标签文本。

我们希望强制规定,当使用Togglable组件时,必须给按钮标签文本prop一个值。

组件的预期和要求的prop可以用prop-types包来定义。让我们安装这个包。

npm install prop-types

我们可以把buttonLabelprop定义为强制或required字符串型prop,如下所示。

import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ..
})

Togglable.propTypes = {
  buttonLabel: PropTypes.string.isRequired
}

如果该prop未被定义,控制台将显示以下错误信息。

fullstack content

尽管有PropTypes的定义,应用仍然可以工作,没有任何东西强迫我们定义prop。请注意,给浏览器控制台留下任何红色输出是非常不专业的。

我们也给LoginForm组件定义PropTypes。

import PropTypes from 'prop-types'

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
    // ...
  }

LoginForm.propTypes = {
  handleSubmit: PropTypes.func.isRequired,
  handleUsernameChange: PropTypes.func.isRequired,
  handlePasswordChange: PropTypes.func.isRequired,
  username: PropTypes.string.isRequired,
  password: PropTypes.string.isRequired
}

如果传递的prop的类型是错误的,例如,如果我们试图将handleSubmitprop定义为字符串,那么这将导致以下警告。

fullstack content

ESlint

在第三章节,我们将ESlint代码风格工具配置到后端。让我们把ESlint也用在前端。

Vite 默认将 ESlint 安装到项目中,所以我们剩下要做的就是在 .eslintrc.cjs 文件中定义我们想要的配置。

让我们创建一个包含以下内容的 .eslintrc.cjs 文件:

npm install --save-dev eslint-plugin-jest

让我们创建一个.eslintrc.js文件,内容如下。

module.exports = {
  root: true,
  env: {
    browser: true,
    es2020: true,
  },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  settings: { react: { version: '18.2' } },
  plugins: ['react-refresh'],
  rules: {
    "indent": [
        "error",
        2  
    ],
    "linebreak-style": [
        "error",
        "unix"
    ],
    "quotes": [
        "error",
        "single"
    ],
    "semi": [
        "error",
        "never"
    ],
    "eqeqeq": "error",
    "no-trailing-spaces": "error",
    "object-curly-spacing": [
        "error", "always"
    ],
    "arrow-spacing": [
        "error", { "before": true, "after": true }
    ],
    "no-console": 0,
    "react/react-in-jsx-scope": "off",
    "react/prop-types": 0,
    "no-unused-vars": 0    
  },
}

注意:如果你将Visual Studio Code与ESLint插件一起使用,你可能需要添加额外的工作区设置,以便它能够工作。如果你看到加载插件reaction失败。无法找到模块"eslint-plugin-react"需要额外的配置。添加一行 `"eslint.workingDirectories":[{ "mode": "auto" }]到工作区的settings.json中,似乎可以工作。更多信息见这里

让我们创建.eslintignore文件,在版本库根目录中加入以下内容

node_modules
dist
.eslintrc.cjs
vite.config.js

现在目录buildnode_modules将在检查时被跳过。

让我们也创建一个npm脚本来运行lint。

{
  // ...
  {
    "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 db.json",
    "eslint": "eslint ."  },
  // ...
}

组件Togglable导致一个看起来很讨厌的警告 组件定义缺少显示名称

fullstack content

react-devtools也显示出该组件没有名字。

fullstack content

幸运的是,这很容易解决

import { useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ...
})

Togglable.displayName = 'Togglable'
export default Togglable

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