# 快速入门 ## 创建和嵌套组件 React应用由组件组成,每个组件都是UI的一部分,组件拥有自己的外观和逻辑。 ### 组件是返回标签的js函数 ```js function MyButton() { return ( ); } ``` 上述已经声明了MyButton组件,可以将该组件嵌套到另一个组件中 ```js export default function MyApp() { return (

Welcome to my app

); } ``` > react组件必须以大写字母开头,而html组件则是必须以小写字母开头,可以通过组件开头字母的大小写来区分html组件和react组件 ## jsx编写标签 上述返回标签的语法被称为jsx,大多数react项目都支持jsx。 jsx比html更加严格: 1. 所有的标签都要有闭合标签(例如`
`) 2. 组件不能返回多个标签,只能返回一个标签,如果存在多个,必须将其包含到一个公共的父级`
`或`<>`中 ```js function AboutPage() { return ( <>

About

Hello there.
How do you do?

); } ``` ## 添加样式 React中,可以在标签中添加`className`属性来添加样式,其和html中的`class`工作方式相同 ```html ``` ```css /* In your CSS */ .avatar { border-radius: 50%; } ``` ## 显示数据 在jsx中,标签位于js中,而可以在标签内通过`{}`来计算js表达式并将其填充到标签中 ```js return (

{user.name}

); ``` jsx还可以将表达式的值传递给标签属性,通过`{}`传递表达式的值 ```js return ( ); ``` 同时,可以通过js表达式来复制css ```js const user = { name: 'Hedy Lamarr', imageUrl: 'https://i.imgur.com/yXOvdOSs.jpg', imageSize: 90, }; export default function Profile() { return ( <>

{user.name}

{'Photo ); } ``` ## 条件渲染 在React中,没有特殊语法编写条件,故而可以在js中通过if条件引入jsx: ```js let content; if (isLoggedIn) { content = ; } else { content = ; } return (
{content}
); ``` 也可以使用如下方式: ```js
{isLoggedIn ? ( ) : ( )}
``` ## 渲染列表 可以通过如下方式将js数组渲染为列表 ```js const products = [ { title: 'Cabbage', id: 1 }, { title: 'Garlic', id: 2 }, { title: 'Apple', id: 3 }, ]; const listItems = products.map(product =>
  • {product.title}
  • ); return (
      {listItems}
    ); ``` 上述示例中,`
  • `有一个key属性,对于列表中的每个属性,都应该传递给其一个字符串或数字的key,用于在兄弟节点之间唯一标识该元素。 ## 响应事件 可以在组件中声明事件处理函数来响应事件 ```js function MyButton() { function handleClick() { alert('You clicked me!'); } return ( ); } ``` ## 更新界面 如果希望组件维护状态信息,可以通过导入useState来完成 ```js import { useState } from 'react'; function MyButton() { const [count, setCount] = useState(0); ``` 其中,count记录当前的状态,而setCount则是用于改变状态的函数,可以为数组中变量取任何名称。 ```js import { useState } from 'react'; export default function MyApp() { return (

    Counters that update separately

    ); } function MyButton() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return ( ); } ``` ## 使用hook 以`use`开头的函数被称为hook,useState是react提供的一个内置hook。 hook比普通函数更为严格,只能在组件顶层或其他hook的顶层调用hook。如果想要在条件或循环中使用hook,请新建一个组件并在组件内部使用。 ## 组件之间共享数据 如果想要将状态在多个组件之间共享,需要将状态提升存储到最近的公共父组件中 ```js export default function MyApp() { const [count, setCount] = useState(0); function handleClick() { setCount(count + 1); } return (

    Counters that update together

    ); } function MyButton({ count, onClick }) { return ( ); } ``` 此时,由MyApp传递给MyButton的值称之为prop ## jsx展开传递props 如果父组件想要将接收到的props全部传递给子组件,无需在子组件上列出props中全部属性,可以使用`...props`来进行展开 ```js function Profile(props) { return (
    ); } ``` ## 将jsx作为子组件传递 当采用如下形式时: ```js ``` 其中,父组件接收到的prop中,children将代表接受到的子组件内容 ```js import Avatar from './Avatar.js'; function Card({ children }) { return (
    {children}
    ); } export default function Profile() { return ( ); } ``` 此时,想要嵌套在父组件内部的子组件可以通过props.children来访问。 ## 条件渲染 如果在某些条件下,组件不想显示任何内容,可以返回null ```js function Item({ name, isPacked }) { if (isPacked) { return null; } return
  • {name}
  • ; } ``` ## 组件的渲染和提交 ### 组件渲染的原因 - 组件的初次渲染 - 组件(或先祖组件)的状态发生了变化 #### 初次渲染 当引用启动时,会触发初次渲染 #### 状态更新时重新渲染 当初次渲染完成后,可以通过set函数来更新state并触发渲染。更新组件状态会将重新渲染加入到队列。 对于初次渲染,React会调用根部组件的方法来进行渲染,而对于状态更新触发的渲染,react只会调用状态更新对应组件的函数 > 对于组件的渲染,其过程是递归的。如果待渲染的组件中包含了子组件,那么react会对子组件进行渲染,对子组件中包含的孙组件也同样。渲染会对组件子树中所有的组件进行渲染。 #### 渲染差异 React只会在渲染之间存在差异时才会更改React节点,如一个节点,如果渲染之间从父组件接受到了不同的props,此时该组件才会发生重新渲染。 ## state快照 在渲染函数中,state的快照是不可变的,即使在调用setState函数将更新state后,旧组件中获取的state值仍然没有变化。 ```js // 即使调用setTitle,此快照中的title值在setTitle之前和之后仍然没有变化 // ,而新渲染的组件title初始值则为改变之后的值 function Title() { const [title,setTitle]=useState('Morning News'); return (

    {title}

    ) } ``` ## 向setState中传入更新函数 react支持向setState中传入更新函数而不是更新后的值,例如 ```js setNumber(n => n + 1); setNumber(n => n + 1); setNumber(n => n + 1); ``` 此时react会将上述三个函数以此加入到队列中,并在下次渲染时遍历执行队列中的函数。 ## 更新state对象 如果向useState中传入object,那么在调用setState函数时,则是应该传入一个新的对象,而不是在原有state对象上进行更新。 **应该将state对象看作是不可变的,调用setState时,应该创建一个state对象的副本,并且修改副本对象后用副本对象的值去更新setState方法。** ```js // 如果想要修改对象中的某个字段,可以使用如下方法 let oldObj = { name : 'kazusa', isMale: false, }; let newObj = { // 展开对象中的属性 ...odlObj, // 新设置某个属性,用于覆盖旧的值 name:'mashiro', } ``` ### 修改嵌套对象中的属性 ```js setPerson({ ...person, // Copy other fields artwork: { // but replace the artwork ...person.artwork, // with the same one city: 'New Delhi' // but in New Delhi! } }); ``` ## Immer 如果要修改深层嵌套的state值,可以引入Immer库来完成对象的拷贝工作。通过Immer库,可以无需关心对象不可变,而是直接以可变的语法来修改对象属性,而Immer会实际帮忙处理对象不可便的工作。 ```js // 通过该import代替react中useState import { useImmer } from 'use-immer'; // 然后可以直接修改对象中的值 const [person, updatePerson] = useImmer({ name: 'Niki de Saint Phalle', artwork: { title: 'Blue Nana', city: 'Hamburg', image: 'https://i.imgur.com/Sd1AgUOm.jpg', } }); function handleNameChange(e) { updatePerson(draft => { draft.name = e.target.value; }); } ``` ## 更新state数组 和object一样,在react中数组应该也要是不可变的,在更新state中的值时,也应该再创建一个新的数组,并用新的数组值设置 ## 重新渲染时组件状态的保存 只要一个组件还在**UI树中相同的位置**,那么react就会保存其state;如果组件被移除,或另有一个类型的组件被渲染在相同的位置,那么react将会丢弃旧组件的state。 如果不同同类型组件被渲染在相同位置,state仍然会被保存。 ### 在UI树结构的同一位置渲染不同组件 如果在两次渲染之间,在UI树的同一位置渲染了相同类型的组件,那么默认情况下两次ui组件的state相同的。 如果要显式指定两次渲染的组件是不同的,各自有独立的state,可以为两次渲染的组件指定不同的key: ```js import { useState } from 'react'; export default function Scoreboard() { const [isPlayerA, setIsPlayerA] = useState(true); return (
    {isPlayerA ? ( ) : ( )}
    ); } function Counter({ person }) { const [score, setScore] = useState(0); const [hover, setHover] = useState(false); let className = 'counter'; if (hover) { className += ' hover'; } return (
    setHover(true)} onPointerLeave={() => setHover(false)} >

    {person} 的分数:{score}

    ); } ``` > 注意,key并不需要全局唯一,key只用于指定父组件内部的顺序 ### 前后两次组件渲染位于不同UI树位置 如果前后两次组件渲染时位于不同的ui树位置,如果要保持前后两次渲染组件state相同,可以为前后两次渲染指定相同的key,那么在前后两次渲染时,即使ui树位置不同,state仍然会延续相同。 state和树中位置相关,例如 ```js // 第一次渲染 return (
    ) // 第二次渲染 return (
    ) ``` 在上述两次渲染中,`item a`和`item b`父节点`
  • `的顺序虽然发生了改变,但是能够通过`
  • `元素的key属性来区分,故而`item a`和`item b`的state仍然会保持原样。显示效果为列表第一行元素和第二行元素发生了对调。 ## Reducer reducer是一个函数,接受两个参数: - 当前state - action,代表改变状态的参动作,数据结构可以自定义 reducer方法的返回值是一个新的state,react会将旧的状态设置为新的状态。 ### Reducer使用 ```js import { useReducer } from 'react'; // 用于替换useState // useReducer接受两个参数,一个reducer函数,一个state的初始值 // 其会返回有状态的值和一个dispatch函数,dispatch函数用于分发action给reducer const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); // 之后可以向dispatch函数提交action function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task, }); } // reducer函数会处理action function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [ ...tasks, { id: action.id, text: action.text, done: false, }, ]; } case 'changed': { return tasks.map((t) => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter((t) => t.id !== action.id); } default: { throw Error('未知 action: ' + action.type); } } } ``` ## Context 如果需要在先祖节点和子孙节点间隔多级时,从先祖节点向子孙节点传递数据,可以使用Context. Context使用如下: ### 创建Context ```js import { createContext } from 'react'; export const LevelContext = createContext(1); ``` ### 子孙组件从Context中获取值 ```js import { useContext } from 'react'; import { LevelContext } from './LevelContext.js'; export default function Heading({ children }) { const level = useContext(LevelContext); // ... } ``` ### 先祖节点提供Context ```js import { LevelContext } from './LevelContext.js'; export default function Section({ level, children }) { return (
    {children}
    ); } ```