目录
The components children, aka. props.children
References to components with ref
Displaying the login form only when appropriate
【在合适的时候展示登录表单】
让我们修改应用,让登录表单在默认情况下不显示
而当用户点击登录按钮时,登录表单再出现
用户可以通过单击 cancel 按钮关闭登录表单
首先将登录组件解耦出来:
import React from 'react'
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
状态以及所有相关的函数都在组件外进行定义,并作为属性传递给组件。
注意,属性是通过变量解构出来的,而不是如下这种方式编写:
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 对象的 props.handleSubmit 属性时,属性被直接赋值给它们自己的变量。
一个快速的实现方式是改变 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>
我们再次使用三元运算符。如果 loginVisible 是 true,组件的 CSS 规则为:
display: 'none';
如果 loginVisible 是 false, display 不会接受任何与组件可见性相关的值。
The components children, aka. props.children
【组件的 children,又叫 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 的术语来说, LoginForm 组件是 Togglable 的子组件。
任何我们想要打开或关闭的组件都可以通过 Togglable 进行包裹,例如:
<Togglable buttonLabel="reveal">
<p>this line is at start hidden</p>
<p>also this is hidden</p>
</Togglable>
Togglable 组件的代码如下:
import React, { 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>
children被 React 自动添加了,并始终存在,只要这个组件定义了关闭标签 />
<Note
key={note.id}
note={note}
toggleImportance={() => toggleImportanceOf(note.id)}
/>
这时 props.children 是一个空的数组。
Togglable 组件可被重用,我们可以用它创建新的切换可见性的功能,如对添加 Note 的表单添加类似的功能。
在这之前,我们把创建 Note 的表单解耦到自己的组件中。
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>
)
}
export default NoteForm
把组件定义在 Togglable 组件中
<Togglable buttonLabel="new note">
<NoteForm
onSubmit={addNote}
value={newNote}
handleChange={handleNoteChange}
/>
</Togglable>
State of the forms
【表单的状态】
应用的状态当前位于 App 组件中。
React文档阐述了关于在哪里放置状态:
Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.
通常,几个组件需要反映相同的变化数据。 我们建议将共享状态提升到它们最接近的共同祖先。
如果我们考虑一下表单的状态,例如一个新便笺的内容在创建之前,App 组件实际上并不需要它做任何事情。
可以将表单的状态移动到相应的组件中。
便笺的组件变化如下:
import React, {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>
)
}
newNote state 属性和负责更改它的事件处理程序已经从 App 组件移动到负责记录表单的组件。
现在只剩下一个props,即 createNote 函数,当创建新便笺时,表单将调用该函数。
用于创建新便笺的 addNote 函数接收一个新便笺作为参数,该函数是我们发送到表单的唯一props:
const App = () => {
// ...
const addNote = (noteObject) => {
noteService
.create(noteObject)
.then(returnedNote => {
setNotes(notes.concat(returnedNote))
})
}
// ...
const noteForm = () => (
<Togglable buttonLabel='new note'>
<NoteForm createNote={addNote} />
</Togglable>
)
// ...
}
References to components with ref
【引用具有 ref 的组件】
我们当前的实现还不错,但有个地方可以改进
当我们创建了一个新的 Note,我们应当隐藏新建 Note 的表单。当前这个表单会持续可见,但隐藏这个表单有个小问题。可见性是透过Togglable 组件的visible 变量来控制的,我们怎么从外部进行访问呢?
从父组件来关闭这个表单有许多方法,我们使用 React 的 ref机制,它提供了一个组件的引用。
把 App 组件按如下修改:
import React, { useState, useRef } from 'react'
const App = () => {
// ...
const noteFormRef = useRef()
const noteForm = () => (
<Togglable buttonLabel='new note' ref={noteFormRef}> <NoteForm createNote={addNote} />
</Togglable>
)
// ...
}
useRef 方法就是用来创建 noteFormRef 引用,它被加到了能够控制表单创建的 Togglable 组件, noteFormRef 变量就代表了组件的引用。
同样要修改 Togglable 组件:
import React, { useState, useImperativeHandle } from 'react'
const Togglable = React.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 函数调用。利用这种方式可以访问赋给它的引用。
组件利用useImperativeHandle Hook来将toggleVisibility 函数能够被外部组件访问到。
现在可以在 Note 创建后,通过调用 noteFormRef.current.toggleVisibility() 控制表单的可见性了
const App = () => {
// ...
const addNote = (noteObject) => {
noteFormRef.current.toggleVisibility() noteService
.create(noteObject)
.then(returnedNote => {
setNotes(notes.concat(returnedNote))
})
}
// ...
}
useImperativeHandle函数是一个 React hook,用于定义组件中的函数,该组件可以从组件外部调用。
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>
三个单独的组件都有自己的状态:
ref 属性用于为变量 togglable1、 togglable2 和 togglable3 中的每个组件分配一个引用。
PropTypes
Togglable 组件假定使用者会通过 buttonLabel 属性获传递按钮的文本。 如果我们忘记给组件定义:
<Togglable> buttonLabel forgotten... </Togglable>
应用会运行正常,但浏览器呈现一个没有 label text 的按钮。
如果希望使用 Togglable 组件时强制给按钮一个 label text 属性值,可以通过 prop-types 包来定义:
npm install prop-types
可以定义 buttonLabel 属性定义为 mandatory,或按如下加入required 这种字符串类型的属性:
import PropTypes from 'prop-types'
const Togglable = React.forwardRef((props, ref) => {
// ..
})
Togglable.propTypes = {
buttonLabel: PropTypes.string.isRequired
}
如果这时属性是 undefined,控制台就会展示如下的错误信息
虽然应用程序仍然可以工作,没有强迫我们定义 PropTypes。 但通过控制台来提醒,因为不处理红色警告是非常不专业的做法。
让我们 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 的类型是错误的。例如尝试定义 handleSubmit 成 string,那结果会出现如下警告:
ESlint
ESlint代码控制样式。
Create-react-app 已经默认为项目安装好了 ESlint, 所以我们需要做的就是定义自己的.eslintrc.js 文件
注意: 不要运行 eslint-- init 命令。 它将安装与 create-react-app 创建的配置文件不兼容的最新版本的 ESlint!
下面开始测试前端,为避免不想要和不相关的 lint 错误,先安装eslint-plugin-jest 库:
npm install --save-dev eslint-plugin-jest
为 .eslintrc.js 添加如下内容
module.exports = {
"env": {
"browser": true,
"es6": true,
"jest/globals": true
},
"extends": [
"eslint:recommended",
"plugin:react/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2018,
"sourceType": "module"
},
"plugins": [
"react", "jest"
],
"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/prop-types": 0
},
"settings": {
"react": {
"version": "detect"
}
}
}
创建一个 .eslintignore 添加如下内容:
node_modules
build
现在 build 和 node_modules 这两个文件夹就不会被 lint 到了
同样为 lint 创建一个 npm 脚本:
{
// ...
{
"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 导致了一些烦人的警告:组件定义缺少显示名:
React-devtools 还显示组件没有名称:
这个问题很容易解决
import React, { useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'
const Togglable = React.forwardRef((props, ref) => {
// ...
})
Togglable.displayName = 'Togglable'
export default Togglable