2022年学React,一篇就够了!

·  阅读 4484
2022年学React,一篇就够了!

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 9 天,点击查看活动详情

React 作为全球最流行的 UI 框架,它非常庞大,而且学习成本也比较高,学习它可能要花费很长时间。
特别是在 16.8.0 版本之后,React 支持使用 Hooks 的开发范式,让 React 更加复杂。
本文力图从初学者的角度,围绕基本概念、组件、Hooks 三个 React 核心维度出发,将 React 中 90% 以上的实用功能和常用概念讲清楚,帮助你快速掌握 React。
如果你是个 React 老手,也可以查漏补缺,留作不时之需。相信阅读这篇文章会比你去翻阅 React 文档或者去 Google 解决问题的效率要快得多。

初始化项目

安装

开发 Web 应用时,React 通常需要搭配 ReactDOM 一起使用,下面是使用 npm 安装 React 的命令。

npm i react react-dom
复制代码

使用 CRA 创建项目

创建 React 项目最快捷的方式就是使用 Create-React-App(简称 CRA)。

npx create-react-app APP_NAME
复制代码

基本概念

元素

React 元素和普通的 HTML 元素写法一致,你可以在 React 中用 HTML 的语法创建任何元素。

<div>I'm JSX</div>
复制代码

这种语法被称作 JSX。
实际上,JSX 语法只是 JavaScript 函数的语法糖,所以它还是需要使用类似 babel 之类的工具进行编译。
和 HTML 不同的一点是,JSX 的自闭合标签必须用斜杠闭合。

<img src="/img.png" />
复制代码

元素属性

除了元素的语法稍有不同外,在元素属性上,JSX 和 HTML 也有些许差别。
因为 JSX 是 JavaScript,按照 JavaScript 的命名约定,通常会使用驼峰命名法,所以在 JXS 中的元素属性都应该改用驼峰命名法(事件也是同理)。

<input defaultChecked defaultValue="o" onInput={()=>{}} />
复制代码

除了普通的属性外,还有一个特殊的属性需要注意。
这个特殊的属性是 class,因为在 JavaScript 中,class 是关键字,所以需要改用 className。

<div className="class"></div>
复制代码

元素样式

在 JSX 中使用内联样式时,我们不能使用字符串,而应该使用对象。

<div style={{ fontSize: '1.25rem', textAlign: 'center' }}>Hello, JSX</div>
复制代码

组件

我们可以将多个元素组合成一个 React 组件。
在过去,React 编写组件的方式是使用 Class,但现在更推荐的方式是使用 Function。
Function 组件和普通的函数类似,但是有两点区别:

  1. 组件名以大写开头。
  2. 组件需要返回 JSX 元素。

下面是一个最简单的组件。

function MyComponent() {
  return <div>Hello Component!</div>
}
复制代码

Fragment

React 要求所有的组件都应该有一个根元素,也就是说一个组件不可以同时包含多个同级元素。
下面是一个反例,它会认为是语法错误:

function Sign() {
  return (
    <input />
    <input />
    <button />
  )
}
复制代码

JSX 为了处理这种情况,在 React 16.2.0 之后,提供了 Fragment 组件。
我们可以这样写:

function Sign() {
  return (
    <React.Fragment>
      <input />
      <input />
      <button />
    </React.Fragment>
  )
}
复制代码

Fragment 还可以被简写为 <></>,所以我们通常会这么写:

function Sign() {
  return (
    <>
      <input />
      <input />
      <button />
    </>
  )
}
复制代码

props

组件之间可以相互包裹,这样就形成了父子关系。

<Parent>
  <Child />
</Parent>
复制代码

我们可以在父组件中给子组件传递数据,我们称这种数据为 props。
传递 props 的语法和普通的属性类似,但区别是可以传递对象。

<Child name='章三', age={19} />
复制代码

子组件中通过函数的参数来接收 props 。
在组件中使用 JavaScript 中的变量需要使用花括号包裹。

function Child(props) {
  return <div>
    <span>{props.name}</span>
    <span>{props.age}</span>
  </div>
}
复制代码

我们还可以通过对象解构的语法让代码更加简单。

function Child({ name, age }) {
  return <div>
    <span>{name}</span>
    <span>{age}</span>
  </div>
}
复制代码

如果想将当前的 props 全部传递给子组件,有一种相当简单的方法。

function Parent(props) {
  return <Child {...props} />
}
复制代码

这样就可以将 Parent 的所有 props 传递给 Child。

children

我们可以在组件中放入其他元素或组件。
这种方式被放置到中间的元素或组件被称为子组件(children)。
被嵌入的元素或组件会挂载到 props 对象上,我们通过 props.children 的方式就可以访问到子组件。

<Parent>
  <Child />
</Parent>

function Parent({children}) {
  return children // children 等同于 <Child />
}
复制代码

条件渲染

React 可以根据某些条件进行选择展示或隐藏哪些内容。
最简单的方式是使用 if 语句。

function App() {
  const { isLoading, isError, data } = fetchData()  
  if(isError) {
    return <Error />
  }
  if(isLoading) {
    return <Loading />
  }
  return <div>{data}</div>
}
复制代码

如果在组件嵌套中使用条件,就需要使用三元运算符了,三元运算符需要包括到花括号中。

function App() {
  const { isLoading, isError, data } = fetchData()
  return (<Layout>
      {
        isError ? <Error />:
        isLoading ? <Loading />:
          <div>{data}</div>
      }
    </Layout>)
}
复制代码

除了三元运算符,还可以使用短路运算符,在一些场景下,可读性更高。

function App() {
  const { isLoading, isError, data } = fetchData()
  return (<Layout>
      {isError && <Error />}
      {isLoading && <Loading />}
      <div>{data}</div>
    </Layout>)
}
复制代码

列表渲染

我们还可以将数据列表渲染成组件。
最常见的方式是通过数组的 map 方法来渲染 React 组件。

const users = ['章三', '李四', '王五']

function App() {
  return (
    <>
      {users.map(user => <div key={user}>{user}</div>)}
    </>
  )
}
复制代码

需要注意,我们进行列表渲染时,需要给每一个元素设置一个 key,而且这个 key 必须保持唯一。

HTML 字符串渲染

当我们已经有了一段 HTML 字符串后,需要直接渲染到页面上。
在 React 中实现的方式是使用 dangerouslySetInnerHTML 属性,它的值为一个对象,对象的 __html 属性设置为 HTML 字符串。

const htmlString = '<p>I'm HTML String</p>'

<div dangerouslySetInnerHTML={{ __html: htmlString }}></div>
复制代码

Context

当我们的组件嵌套层级过深时,两个相邻过远的组件之间传递 props 就会很麻烦,我们需要让在中间的所有组件都去接收这个它们本来就用不到的 props。

function App() {
  const username = '章三'
  return <Layout username={username}>
    <div>Hello</div>
  </Layout>
}

// Layout 接收 username 就很多余
function Layout({ username }) {
  return <User username={username}></User>
}

function User({ username }) {
  return <h1>{username}</h1>
}
复制代码

为了解决这种问题,React 提供了 Context,让我们的组件可以脱离 props 来共享数据。
创建 Context 使用 React 的 createContext 函数,它返回一个 Context 对象。
Context 对象具有 Provider 和 Consumer 两个属性,它们都是组件。
我们使用 Provider 组件来包裹需要传递数据的根组件,然后在需要使用数据的位置使用 Consumer 组件包裹,以此来获取数据。

const UserNameContext = React.createContext('')

function App() {
  const username = '章三'
  return <UserNameContext.Provider value={username}>
    <Layout>
    <div>Hello</div>
  </UserNameContext.Provider>
}

function Layout() {
  return <User />
}

function User() {
  return <UserNameContext.Consumer>
    {username => <h1>{username}</h1>}
  </UserNameContext.Consumer>
}
复制代码

在使用 Context 之前,我们需要思考组件是否进行了真正合理的设计。

组件

在基本概念中有提到组件的概念,这里对组件进行更细致全面的讲解。

class 组件与 function 组件

React 中有类组件(class)和函数组件(function)两种组件。
class 组件:

class MyComponent extends React.Component {
  render() {
    return <div>hello</div>
  }
}
复制代码

类组件需要继承 React.Component 组件,并且在 render 方法中返回要渲染的 JSX。
function 组件:

function MyComponent () {
  return <div>hello</div>
}
复制代码

两者的作用是一致的。
这篇文章中会以函数组件为主。因为目前 React 的最佳实践就是函数组件。

纯组件

纯函数是一种性能优化手段,使用纯组件,可以在 props 和 state 都没有发生变化时避免无意义的渲染。
类组件是通过继承 React.PureComponent 来实现的:

class MyComponent extends React.PureComponent {
  render(){
    return <div>I'm Pure Component</div>
  }
}
复制代码

函数组件通过 React.memo 这个高阶组件来实现的,memo 是 16.6.0 增加的 API。

function MyComponent() {
  return <div>I'm Pure Component</div>
}

const PureComponent = React.memo(MyComponent)
复制代码

设置 props 默认值

有时组件需要为 props 设置默认值,通常有两种方式。
默认参数:

function MyComponent (props = { name: '章三' }) {
  return <div>hello { props.name }</div>
}
复制代码

设置 defaultProps 属性:

function MyComponent (props) {
  return <div>hello { props.name }</div>
}

MyComponent.defaultProps = {
  name: '章三'
}
复制代码

子组件触发事件给父组件

虽然 React 中没有事件的概念,但是父组件可以传递函数给子组件,子组件在进行某些操作时触发这个回调函数,并将父组件所需要的数据通过参数的方式传递过去。

function App() {
  const submit = (data) => fetch('/api', { data })
  return <Form onSubmit={submit} />
}

function Form({ onSubmit}) {
  const [data, setData] = useState()
  return <div>
      <!-->表单元素<-->
      <button onClick={() => onSubmit(data)}>提交</button>
    </div>
}
复制代码

高阶组件

高阶组件(HOC)是一种复用组件逻辑的技巧。
组件是将 props 转换为 UI,高阶组件是将组件转换为另一个组件。
使用 HOC 可以解决关注点的问题,让需要被解决的问题集中到 HOC 组件中。
它不是某个 API,而是一种编程模式。
它的原理是接受一个组件作为参数,并返回一个生成新组件的函数。
下面是一个处理 Loading 的高阶组件。

function MyComponent (props) {
  return <div>hello { props.name }</div>
}

function Loading () {
  return <div>loading...</div>
}

function withLoading ({ children }) => {
  return ({ isLoading }) => {
    if(isLoading) return <Loading />
    return children
  }
}

const WithLoging = withLoading(MyComponent)
复制代码

Portal 组件

有时我们需要将子组件渲染到父组件之外,比如开发全局提示或全局对话框。
这时就需要使用 createPortal 方法来实现。

function Modal({ children }) {
  return ReactDOM.createPortal(
    children,
    document.getElementById('root')
  )
}
复制代码

克隆组件

有时候我们需要根据样板组件克隆一个新的 React 组件。
这通常在开发复杂组件时会用到。
我们无法改变组件的 children,但是可以通过对它进行克隆来改变原有组件的 props、key 和样式。

function App({ children }) {
  const cloneChildren = React.cloneElement(children, {
    // 可以在这里修改 props
  })
  return cloneChildren
}
复制代码

验证组件

我们将组件作为参数传递时,需要验证参数是否是一个 React 组件。
React.isValidElement 就可以做这件事,它返回一个 boolean 值。

function Wrap(children) {
  const isElement = React.isValidElement(children)
  // ...
}
复制代码

渲染组件

我们可以在老旧的项目中的某一部分使用 React,而不是在整个项目中使用 React。比如我们想在一个 jQuery 开发的项目中逐步使用 React。
ReactDOM 提供了 render 函数。我们只需要找到一个 DOM,就可以将 React 的组件挂载到 DOM 上面。

const container = document.getElementById('container')
ReactDOM
  .render(<Component />, 
          container,
          () => { console.log('挂载成功!') }
         )
复制代码

React 18 的最新 API 是 createRoot,再通过创建出来的 root 对象的 render 方法进行渲染。语法上与之前版本大同小异。

const root = ReactDOM.createRoot(container)

root.render(<Component />)
复制代码

卸载组件

既然可以将组件渲染到 DOM 中,那也需要有一个对应的 API 将 React 组件从 DOM 中卸载的 API。那就是 unmountComponentAtNode。

unmountComponentAtNode(container)
复制代码

React 18 最新的 API 是 root.unmont。

root.unmont()
复制代码

Hooks

在 React 16.8 之后,React 提供了 Hooks 机制,通过 Hooks 可以创建出带有状态的函数组件,而不再需要使用笨重的 Class 来创建组件。
在今天,我们应该避免再使用 Class 组件,而应该全面拥抱 Hooks 组件。
Hooks 有几个规则:

  • Hooks 以 use 前缀开头
  • 只能在 React 函数组件中使用
  • 只能在 React 函数组件的顶层使用
  • 不能根据条件使用

下面讲解 React 中的几个 Hooks。

  • useState
  • useEffect
  • useRef
  • useContext
  • useCallback
  • useMemo
  • useReducer
  • useLayoutEffect

useState

通过外部传递的数据被称作 props,组件自身的数据被称作 state。
使用 useState 可以创建 state。
state 与普通变量的区别是:当 state 发生变化时,组件会被重新渲染。
useState 的用法如下:

const [stateValue, setStateValue] = useState(initialValue);
复制代码

调用它时需要传递一个初始值;它会返回一个数组,数组中包含两个元素,第一个是当前状态,第二个是更新状态的函数。
当我们调用了更新状态的函数后,组件会重新渲染。
下面是一个计数器的例子,通过它可以理解 useState 的实际用法。

import { useState } from 'react'

function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={()=> setCount(count+1)}>count: {count}</button>
}
复制代码

useEffect

如果我们需要和组件外部的东西进行交互时,需要使用 useEffect,比如后端接口。
useEffect 顾名思义,就是执行副作用,副作用指的是存在于我们的程序之外并且没有可以预测结果的操作。
useEffect 需要两个参数,第一个是副作用函数,第二个是依赖项数组。每当依赖项数组发生变化时,副作用函数会重新执行。

useEffect(() => {}, [])
复制代码

下面是获取博客列表数据的例子。

import { useEffect } from 'react';

function PostList() {
	 const [posts, setPosts] = useState([]);

   useEffect(() => {
	   fetch('https://jsonplaceholder.typicode.com/posts')
       .then(response => response.json())
       .then(posts => setPosts(posts));
   }, []);

   return posts.map(post => <div key={post.id}>{post.title}</div>)
}
复制代码

useRef

useRef 的主要用途之一是访问元素。
使用 useRef 的方法很简单,调用它,返回一个值,然后将这个值通过 props 传递给 React 元素。

function MyComponent() {
  const ref = React.useRef();

  return <div ref={ref} />
}
复制代码

一旦将 ref 设置到元素上,就可以通过 ref.current 访问到元素。
需要注意的是,必须等待组件渲染结束后 ref 才有值,否则 ref 是 undefined。

function MyComponent() {
  const ref = React.useRef();
  console.log(ref)// undefined
  
  useEffect(() => {
    console.log(ref.current)// 现在才可以获取到元素
  }, [])

  return <div ref={ref} />
}
复制代码

最后还要注意,ref 不可以设置到组件上,只能设置到元素上。
如果想访问组件内部的元素,我们可以使用 refs 转发。
具体的做法是:

  • 使用 React.forwardRef 包裹函数组件,函数组件的第二个参数就是 ref,将它设置到具体的元素上。
  • 使用 React.createRef 创建一个 ref 对象,设置到外层的组件上。
const FancyButton = React.forwardRef((props, ref) => (
  <button ref={ref} className="FancyButton">
    {props.children}
  </button>
));

const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
复制代码

useContext

useContext 是一种比使用 Context.Consumer 组件来使用 Context 更简单的方式。
语法也很简单,调用 useContext 函数,将 Context 对象作为参数传递进去。
现在我们重写原来的例子:

const UserNameContext = React.createContext('')

function App() {
  const username = '章三'
  return <UserNameContext.Provider value={username}>
    <Layout>
    <div>Hello</div>
  </UserNameContext.Provider>
}

function Layout() {
  return <User />
}

function User() {
  const username = React.useContext(UserNameContext)
  return <h1>{username}</h1>
}
复制代码

useCallback

useCallback 的作用是为了提高性能。
如果我们在函数组件中创建了一些函数,那么当这个组件每次重新渲染时都会重新创建这些函数,这可能会影响程序的性能。
useCallback 接收两个参数,第一个是要被缓存的函数,第二个是一个依赖项数组。当依赖项数组中的任意一个值发生变化时,useCallback 就会重新创建这个函数。
另外,如果将这些函数作为参数传递给了子组件,那么还会导致子组件被重新渲染。
我们可以使用 memo 结合 useCallback 来减少子组件的渲染。

function Button ({ onClick }) {
  console.log("button render");
  return <button onClick={onClick}>count++</button>;
};

const MemoButton = React.memo(Button);

function Counter() {
  const [count, setCount] = React.useState(0);
  
  const onClick = React.useCallback(() => {
    setCount((count) => count + 1);
  }, []);

  return (
    <>
      <p>count:{count}</p>
      <MemoButton onClick={onClick} />
    </>
  );
}

ReactDOM.render(<Counter />, document.getElementById('app'));
复制代码

useMemo

useMemo 和 useCallback 的作用类似,也是用来提高性能的。不同的是,useCallback 是缓存函数,而 useMemo 是缓存值。
有些渲染时需要用到的变量需要进行计算,而计算的过程可能会很消耗性能,所以当组件重新渲染而计算所需要的 state 没有发生变化时,可以避免再次计算。
和 useEffect、useCallback 类似,每当 useMemo 的依赖项发生变化时,它都会重新计算值。
下面是根据 useCallback 中的例子进行改造的例子。新增了 UserInfo 组件,当 Counter 组件中的 count 发生改变时,不会连带 UserInfo 一起更新。

function Button ({ onClick }) {
  console.log("button render");
  return <button onClick={onClick}>count++</button>;
};

const MemoButton = React.memo(Button);

function UserInfo({ userInfo: { name, age } }) {
  console.log('UserInfo render')
  return <div>
    <p>{name}</p>
    <p>{age}</p>
  </div>
}

const MemoUserInfo = React.memo(UserInfo)

function Counter() {
  const [count, setCount] = React.useState(0);
  const [name, setUsername] = React.useState('章三');
  
  const onClick = React.useCallback(() => {
    setCount((count) => count + 1);
  }, []);
  
  const userInfo = React.useMemo(() => {
    return {
      name,
      age: 15
    }
  }, [name])

  return (
    <>
      <MemoUserInfo userInfo={userInfo} />
      <p>count:{count}</p>
      <MemoButton onClick={onClick} />
    </>
  );
}

ReactDOM.render(<Counter />, document.getElementById('app'));
复制代码

useReducer

useReducer 是用来替代 useState 的 Hook,它用来处理复杂的状态逻辑。
如果你用过 redux,那么会比较熟悉。
它接收两个参数,第一个是 reducer,第二个是初始状态。
返回包含两个元素的数组,第一个元素是当前状态,第二个元素是改变状态的 dispatch 函数。这和 useState 很相似。
reducer 是一个函数,它接收两个参数,当前状态 state 和用来改变状态的 action,并返回新的状态。

const initialState = 0

const reducer = (state, action) => {
  switch (action) {
    case 'increment':
      return state + 1
    case 'decrement':
      return state - 1
    case 'reset':
      return initialState
    default:
      return state
  }
}

function Counter() {
  const [count, dispatch] = React.useReducer(reducer, initialState)
  return (
    <div>
      <div>Count: {count}</div>
      <button
        onClick={() => dispatch('increment')}
      >增加</button>
      <button
        onClick={() => dispatch('decrement')}
      >减少</button>
      <button
        onClick={() => dispatch('reset')}
      >重置</button>
    </div>
  )
}

ReactDOM.render(<Counter />, document.getElementById('app'));
复制代码

useLayoutEffect

useLayoutEffect 和 useEffect 很相似,唯一的区别是执行时机不同,useLayoutEffect 是在浏览器进行 paint 和 layout 之后执行,所以利用 useLayoutEffect 可以防止页面闪烁。
最常见的一种情况是更新了一次状态,同时触发 effect 的回调函数,再次更新这个状态,导致短时间内连续更新两次状态。
可以通过下面的活动截止倒计时程序来理解:

function useInterval(callback, delay) {
  const savedCallback = React.useRef();

  React.useEffect(() => {
    savedCallback.current = callback;
  });

  React.useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}

function Draw() {
  const [countdown, setCountdown] = React.useState(3)

  useInterval(()=> setCountdown(countdown - 1), 1000)
  
  React.useLayoutEffect(() => {
    if(countdown <= 0) {
      setCountdown(3)
    }
  }, [countdown])
  return (
    <div>
      <div>活动剩余截止时间:{countdown} 秒</div>
    </div>
  )
}

ReactDOM.render(<Draw />, document.getElementById('app'));
复制代码

活动倒计时每秒会减 1 秒,当被减到 0 秒时,再重置为 3 秒。
如果使用 useEffect,当倒计时被设置为 0 时,会重新渲染页面,并且马上再次将倒计时设置为 3,导致很短的时间页面渲染两次。
useLayoutEffect 的使用场景不多,它会阻塞渲染。通常可以通过逻辑进行避免。

自定义 Hook

我们可以根据自己的需求自定义 Hook,Hook 的作用是逻辑复用。
自定义 Hook 有以下几个点需要注意:

  • Hook 是以 use 开头的函数
  • 函数内部可以调用其他 Hook

useLayoutEffect 中的例子就用到了自定义 Hook useInterval。

function useInterval(callback, delay) {
  const savedCallback = React.useRef();

  // 保存新回调
  React.useEffect(() => {
    savedCallback.current = callback;
  });

  // 建立 interval
  React.useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (delay !== null) {
      let id = setInterval(tick, delay);
      return () => clearInterval(id);
    }
  }, [delay]);
}
复制代码

如果本文对你有所帮助,欢迎点赞评论收藏。
在未来,我还会推出更多关于 React 以及 React 与 TypeScript、Redux、React Router 等框架集成使用的文章。
如果你对 React 技术感兴趣,欢迎关注专栏。

分类:
前端
收藏成功!
已添加到「」, 点击更改