Displaying the login form only when appropriate

在合适的时候显示登录表单
先将LoginForm独立成组件,在loginForm函数中使用内联样式控制是否显示

  1. const App = () => {
  2. const [loginVisible, setLoginVisible] = useState(false)
  3. // ...
  4. const loginForm = () => {
  5. const hideWhenVisible = { display: loginVisible ? 'none' : '' }
  6. const showWhenVisible = { display: loginVisible ? '' : 'none' }
  7. return (
  8. <div>
  9. <div style={hideWhenVisible}>
  10. <button onClick={() => setLoginVisible(true)}>log in</button>
  11. </div>
  12. <div style={showWhenVisible}>
  13. <LoginForm
  14. username={username}
  15. password={password}
  16. handleUsernameChange={({ target }) => setUsername(target.value)}
  17. handlePasswordChange={({ target }) => setPassword(target.value)}
  18. handleSubmit={handleLogin}
  19. />
  20. <button onClick={() => setLoginVisible(false)}>cancel</button>
  21. </div>
  22. </div>
  23. )
  24. }
  25. // ...
  26. }

The components children, aka. props.children

子组件:props.children
新建Togglable组件

  1. import React, { useState } from 'react'
  2. const Togglable = (props) => {
  3. const [Visible, setVisible] = useState(false)
  4. const hideWhenVisible = { display: Visible ? 'none' : '' }
  5. const showWhenVisible = { display: Visible ? '' : 'none' }
  6. const toggleVisibility = () => {
  7. setVisible(!Visible)
  8. }
  9. return (
  10. <div>
  11. <div style={hideWhenVisible}>
  12. <button onClick={toggleVisibility}>{props.buttonLabel}</button>
  13. </div>
  14. <div style={showWhenVisible}>
  15. {props.children}
  16. <button onClick={toggleVisibility}>cancel</button>
  17. </div>
  18. </div>
  19. )
  20. }
  21. export default Togglable

props.children是一个数组,可以在此处插入子组件

  1. <Togglable buttonLabel='login'>
  2. <LoginForm
  3. username={username}
  4. password={password}
  5. handleUsernameChange={({ target }) => setUsername(target.value)}
  6. handlePasswordChange={({ target }) => setPassword(target.value)}
  7. handleSubmit={handleLogin}
  8. />
  9. </Togglable>

Toggable可以重用

  1. <Togglable buttonLabel="new note">
  2. <NoteForm
  3. onSubmit={addNote}
  4. value={newNote}
  5. handleChange={handleNoteChange}
  6. />
  7. </Togglable>

State of the forms

表单状态

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.
通常,几个组件需要反映相同的变化数据。 我们建议将共享状态提升到它们最接近的共同祖先。

把NoteForm的状态移到它自己的组件中

  1. import React, {useState} from 'react'
  2. const NoteForm = ({ createNote }) => {
  3. const [newNote, setNewNote] = useState('')
  4. const handleChange = (event) => {
  5. setNewNote(event.target.value)
  6. }
  7. const addNote = (event) => {
  8. event.preventDefault()
  9. createNote({
  10. content: newNote,
  11. important: Math.random() > 0.5,
  12. })
  13. setNewNote('')
  14. }
  15. return (
  16. <div>
  17. <h2>Create a new note</h2>
  18. <form onSubmit={addNote}>
  19. <input
  20. value={newNote}
  21. onChange={handleChange}
  22. />
  23. <button type="submit">save</button>
  24. </form>
  25. </div>
  26. )
  27. }
  1. const App = () => {
  2. // ...
  3. const addNote = (noteObject) => {
  4. noteService
  5. .create(noteObject)
  6. .then(returnedNote => {
  7. setNotes(notes.concat(returnedNote))
  8. })
  9. }
  10. // ...
  11. const noteForm = () => (
  12. <Togglable buttonLabel='new note'>
  13. <NoteForm createNote={addNote} />
  14. </Togglable>
  15. )
  16. // ...
  17. }

References to components with ref

创建ref引用组件

  1. import React, { useState, useEffect, useRef } from 'react'
  2. const App = () => {
  3. // ...
  4. const noteFormRef = useRef()
  5. const noteForm = () => (
  6. <Togglable buttonLabel='new note' ref={noteFormRef}>
  7. <NoteForm createNote={addNote} />
  8. </Togglable>
  9. )
  10. // ...
  11. }

使用forwardRef和useImperativeHandle配合,使外部能访问组件内部数据

  1. import React, { useState, useImperativeHandle } from 'react'
  2. const Togglable = React.forwardRef((props, ref) => {
  3. const [visible, setVisible] = useState(false)
  4. const hideWhenVisible = { display: visible ? 'none' : '' }
  5. const showWhenVisible = { display: visible ? '' : 'none' }
  6. const toggleVisibility = () => {
  7. setVisible(!visible)
  8. }
  9. useImperativeHandle(ref, () => {
  10. return {
  11. toggleVisibility
  12. }
  13. })
  14. return (
  15. <div>
  16. <div style={hideWhenVisible}>
  17. <button onClick={toggleVisibility}>{props.buttonLabel}</button>
  18. </div>
  19. <div style={showWhenVisible}>
  20. {props.children}
  21. <button onClick={toggleVisibility}>cancel</button>
  22. </div>
  23. </div>
  24. )
  25. })
  26. export default Togglable

使用ref.current访问引用内的数据

  1. const App = () => {
  2. // ...
  3. const addNote = (noteObject) => {
  4. noteFormRef.current.toggleVisibility()
  5. noteService
  6. .create(noteObject)
  7. .then(returnedNote => {
  8. setNotes(notes.concat(returnedNote))
  9. })
  10. }
  11. // ...
  12. }

Exercises 5.5.-5.10.

实现点击按钮显示或隐藏博客内容
image.png

  1. import React, { useState } from 'react'
  2. const Blog = ({ blog }) => {
  3. const [visible, setVisible] = useState(false)
  4. const [buttonLabel, setButtonLabel] = useState('view')
  5. const showStyle = { display: visible ? '' : 'none' }
  6. const toggleVisibility = () => {
  7. setVisible(!visible)
  8. if (visible) {
  9. setButtonLabel('view')
  10. } else {
  11. setButtonLabel('hide')
  12. }
  13. }
  14. const blogStyle = {
  15. paddingTop: 10,
  16. paddingLeft: 2,
  17. border: 'solid',
  18. borderWidth: 1,
  19. marginBottom: 5
  20. }
  21. return (
  22. <div style={blogStyle}>
  23. <div>
  24. {blog.title} {blog.author} <button onClick={toggleVisibility}>{buttonLabel}</button>
  25. </div>
  26. <div style={showStyle}>
  27. <div>{blog.url}</div>
  28. <div>likes {blog.likes} <button>like</button></div>
  29. <div>{blog.author}</div>
  30. </div>
  31. </div>
  32. )
  33. }
  34. export default Blog

更新和删除blog

  1. const update = async (blog, id) => {
  2. const response = await axios.put(`${baseUrl}/${id}`, blog, config)
  3. return response.data
  4. }
  5. const deleteBlog = async (id) => {
  6. await axios.delete(`${baseUrl}/${id}`, config)
  7. }
  1. const removeBlog = async (blog) => {
  2. try {
  3. if (window.confirm('do your wanna delete this blog?')) {
  4. await blogService.deleteBlog(blog.id)
  5. setBlogs(blogs.filter(item => item.id !== blog.id))
  6. }
  7. } catch (error) {
  8. console.log(error)
  9. }
  10. }
  11. const updateLikes = async () => {
  12. const newBlog = { ..._blog, 'likes': _blog.likes + 1 }
  13. try {
  14. await blogService.update(newBlog, blog.id)
  15. setBlog(newBlog)
  16. }
  17. catch (error) {
  18. console.log(error)
  19. }
  20. }

将blogs按likes排序

  1. setBlogs(blogs.sort((a, b) => b.likes - a.likes))

PropTypes

如果忘记给Toggable传递buttonLabel, 程序能正常工作,但是按钮会变成空白

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

我们可以使用prop-type包,给组件设置PropTypes,当传递props与要求不符时,控制台会报错
安装

  1. npm install prop-types

定义组件的proptypes

  1. import PropTypes from 'prop-types'
  2. const Togglable = React.forwardRef((props, ref) => {
  3. // ..
  4. })
  5. Togglable.propTypes = {
  6. buttonLabel: PropTypes.string.isRequired
  7. }

ESlint

Create-react-app 已经默认为项目安装好了 ESlint, 所以我们需要做的就是定义自己的.eslintrc.js 文件
注意: 不要运行 eslint— init 命令。 它将安装与 create-react-app 创建的配置文件不兼容的最新版本的 ESlint!

安装eslint-plugin-jest

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

新建.eslintrc.js文件,添加如下内容

  1. /* eslint-env node */
  2. module.exports = {
  3. "env": {
  4. "browser": true,
  5. "es6": true,
  6. "jest/globals": true
  7. },
  8. "extends": [
  9. "eslint:recommended",
  10. "plugin:react/recommended"
  11. ],
  12. "parserOptions": {
  13. "ecmaFeatures": {
  14. "jsx": true
  15. },
  16. "ecmaVersion": 2018,
  17. "sourceType": "module"
  18. },
  19. "plugins": [
  20. "react", "jest"
  21. ],
  22. "rules": {
  23. "indent": [
  24. "error",
  25. 2
  26. ],
  27. "linebreak-style": [
  28. "error",
  29. "unix"
  30. ],
  31. "quotes": [
  32. "error",
  33. "single"
  34. ],
  35. "semi": [
  36. "error",
  37. "never"
  38. ],
  39. "eqeqeq": "error",
  40. "no-trailing-spaces": "error",
  41. "object-curly-spacing": [
  42. "error", "always"
  43. ],
  44. "arrow-spacing": [
  45. "error", { "before": true, "after": true }
  46. ],
  47. "no-console": 0,
  48. "react/prop-types": 0
  49. },
  50. "settings": {
  51. "react": {
  52. "version": "detect"
  53. }
  54. }
  55. }

如果vscode使用eslint插件报错,Failed to load plugin react: Cannot find module ‘eslint-plugin-react’
在配置文件settings.json中更改

  1. "eslint.workingDirectories": [{ "mode": "auto" }]

在仓库根目录创建文件.eslintignore, 添加下列内容

  1. node_modules
  2. build
  3. .eslintrc.js

在package.json添加scripts

  1. {
  2. "scripts": {
  3. "start": "react-scripts start",
  4. // ...
  5. "eslint": "eslint ."
  6. },

当Eslint报LF和CRLF错误时,将linebreak-style的unix改为windows

  1. "linebreak-style": [
  2. "error",
  3. "windows"
  4. ]

image.png
Eslint提示Togglable组件没有display name
添加下列代码修复:

  1. Togglable.displayName = 'Togglable'

添加Eslint后,yarn start运行程序会报错
在.env文件添加下列2行

  1. DISABLE_ESLINT_PLUGIN=true
  2. ESLINT_NO_DEV_ERRORS=true