REACT V16.8 中加入了 React.FC 配合typescript 使用

一,函数式组件

  • 纯函数
  • 输入props,输出JSX
  • 没有实例
  • 没有生命周期
  • 没有state
  • 不能扩展其它方法
  1. function List (props) {
  2. const { text } = this.props
  3. return (
  4. <div>{text}</div>
  5. )
  6. }

无状态组件的性能比较高,因为它仅是一个函数,而普通组件是一个class

在越来越多的复杂的函数组件的情况下的时候;我们有时候需要的简单的组件;但是需要使用到一些他们的状态的保存和一些生命周期的比变化的 时候我们又不想去使用class的场景的组件;这时候需要怎么办呢;

于是乎在react的16.8中的时候react更新了关于react hooks 的一些东西;

简单的说就是在无状态组件中可以使用生命周期的一些方法;从而使得不需要很重量的代码就可以达到类中的状态管理

名称 解释 写法
useState 用于为函数组件引入状态(state)。纯函数不能有状态,所以把状态放在钩子里面。 const [on, setOn] = useState(false) 状态名改变状态的方法状态的默认值
useContext() 如果需要在组件之间共享状态
useReducer() action 钩子
React 本身不提供状态管理功能,通常需要使用外部库。这方面最常用的库是 Redux组件发出 action 与状态管理器通信。状态管理器收到 action 以后,使用 Reducer 函数算出新的状态,Reducer 函数的形式是(state, action) => newState const [state, dispatch] = useReducer(reducer, initialState); 上面是useReducer()的基本用法,它接受 Reducer 函数和状态的初始值作为参数,返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的dispatch函数。
useEffect()副作用钩子 useEffect()用来引入具有副作用的操作,最常见的就是向服务器请求数据。以前,放在componentDidMount里面的代码,现在可以放在useEffect()。 每当组件参数personId发生变化,useEffect()就会执行。组件第一次渲染时,useEffect()也会执行。 上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()
useMemo() 是定义一段函数逻辑是否重复执行 若第二个参数为空,则每次渲染组件该段逻辑都会被执行,就不会根据传入的属性值来判断逻辑是否重新执行,这样写useMemo()也就毫无意义。 useMemo()是需要有返回值的,并且返回值是直接参与渲染,因此useMemo()是在渲染期间完成的。
useRef() 是因为函数组件没有实例,所以函数组件中无法使用String Ref、Callback Ref、Create Ref,取而代之的是useRef
获取DOM元素的节点
获取子组件的实例
渲染周期之间共享数据的存储(state不能存储跨渲染周期的数据,因为state的保存会触发组件重渲染)
useImperativeHandle
useLayoutEffect
useDebugValue

useState explame

  1. // useState explame
  2. import React, { useState } from "react";
  3. export default function Button() {
  4. const [buttonText, setButtonText] = useState("Click me, please");
  5. // 状态名 改变状态的方法 => 以及对应该状态名的默认的值
  6. // 调用该状态改变的方法,赋值新的状态
  7. function handleClick() {
  8. return setButtonText("Thanks, been clicked!");
  9. }
  10. return <button onClick={handleClick}>{buttonText}</button>;
  11. }

useContext() explame

  1. import React from "react";
  2. // 从react中获取对应的appcontent的组件
  3. const {AppContext,Consumer} = React.createContext({});
  4. constructor (props) {
  5. super(props)
  6. this.state = { theme: 'light' }
  7. }
  8. // AppContext.Provider 包裹在对应的函数中;对应的全局共享的字面量存储在value中
  9. <AppContext.Provider value={{
  10. username: 'superawesome'
  11. }}>
  12. <div className="App">
  13. <Navbar/>
  14. <Messages/>
  15. </div>
  16. </AppContext.Provider>
  17. // 在对应的子组件中通过useContext(AppContext) 获取对应的值
  18. const Navbar = () => {
  19. const { username } = useContext(AppContext);
  20. return (
  21. <div className="navbar">
  22. <p>AwesomeSite</p>
  23. <p>{username}</p>
  24. </div>
  25. );
  26. }
  27. // 子组件中调用
  28. import { Consumer } from "./index";//引入父组件的Consumer容器
  29. render () {
  30. return (
  31. // Consumer 容器,可以拿到上文传递下来的 theme 属性,并可以展示对应的值
  32. <Consumer>
  33. { theme => <div>子组件。获取父组件的值: {theme} </div> }
  34. </Consumer>
  35. )
  36. }

useReducer() explame

  1. import myReducer from Reducer
  2. function App() {
  3. // 它接受 Reducer 函数和状态的初始值作为参数,
  4. // 返回一个数组。数组的第一个成员是状态的当前值,第二个成员是发送 action 的dispatch函数。
  5. const [state, dispatch] = useReducer(myReducer, { count: 0 });
  6. return (
  7. <div className="App">
  8. <button onClick={() => dispatch({ type: 'countUp' })}>
  9. +1
  10. </button>
  11. <p>Count: {state.count}</p>
  12. </div>
  13. );
  14. }

useEffect() explame

上面用法中,useEffect()接受两个参数。第一个参数是一个函数,异步操作的代码放在里面。第二个参数是一个数组,用于给出 Effect 的依赖项,只要这个数组发生变化,useEffect()就会执行。第二个参数可以省略,这时每次组件渲染时,就会执行useEffect()

  1. const Person = ({ personId }) => {
  2. const [loading, setLoading] = useState(true);
  3. const [person, setPerson] = useState({});
  4. useEffect(() => {
  5. setLoading(true);
  6. fetch(`https://swapi.co/api/people/${personId}/`)
  7. .then(response => response.json())
  8. .then(data => {
  9. setPerson(data);
  10. setLoading(false);
  11. });
  12. }, [personId])
  13. if (loading === true) {
  14. return <p>Loading ...</p>
  15. }
  16. return <div>
  17. <p>You're viewing: {person.name}</p>
  18. <p>Height: {person.height}</p>
  19. <p>Mass: {person.mass}</p>
  20. </div>
  21. }

useMemo() explame

若第二个参数为空,则每次渲染组件该段逻辑都会被执行,就不会根据传入的属性值来判断逻辑是否重新执行,这样写useMemo()也就毫无意义。

  1. import React, { memo, useState, useMemo } from "react";
  2. function App() {
  3. const [value, setValue] = useState(0);
  4. const increase = useMemo(() => {
  5. if(value > 2) return value + 1;
  6. }, [value]);
  7. return (
  8. <div>
  9. <Child value={value} />
  10. <button
  11. type="button"
  12. onClick={() => {
  13. setValue(value + 1);
  14. }}
  15. >
  16. value:{value},increase:{increase || 0}
  17. </button>
  18. </div>
  19. );
  20. }
  21. // memo() 定义一个组件是否需要重复执行
  22. const Child = memo(function Child(props) {
  23. console.log('Child render')
  24. return <h1>value:{props.value}</h1>;
  25. });
  26. export default App;

Refs

React 的核心思想是每次对于界面 state 的改动,都会重新渲染整个Virtual DOM,然后新老的两个 Virtual DOM 树进行 diff(协调算法),对比出变化的地方,然后通过 render 渲染到实际的UI界面,
使用 Refs 为我们提供了一种绕过状态更新和重新渲染时访问元素的方法;这在某些用例中很有用,但不应该作为 propsstate 的替代方法。
在项目开发中,如果我们可以使用 声明式 或 提升 state 所在的组件层级(状态提升) 的方法来更新组件,最好不要使用 refs。

函数组件中 useRef() explame

  1. import React, { useEffect, useRef } from 'react';
  2. function App() {
  3. const h1Ref = useRef();
  4. useEffect(() => {
  5. console.log('useRef')
  6. console.log(h1Ref.current)
  7. }, [])
  8. return <h1 ref={h1Ref}>Hello World!</h1>
  9. }
  10. export default App;

管理焦点(如文本选择)或处理表单数据: Refs 将管理文本框当前焦点选中,或文本框其它属性。
在大多数情况下,我们推荐使用受控组件来处理表单数据。在一个受控组件中,表单数据是由 React 组件来管理的,每个状态更新都编写数据处理函数。另一种替代方案是使用非受控组件,这时表单数据将交由 DOM 节点来处理。要编写一个非受控组件,就需要使用 Refs 来从 DOM 节点中获取表单数据。

  1. // 引出了非受控组件 获取dom节点
  2. class NameForm extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.input = React.createRef();
  6. }
  7. handleSubmit = (e) => {
  8. console.log('A name was submitted: ' + this.input.current.value);
  9. e.preventDefault();
  10. }
  11. render() {
  12. return (
  13. <form onSubmit={this.handleSubmit}>
  14. <label>
  15. Name:
  16. <input type="text" ref={this.input} />
  17. </label>
  18. <input type="submit" value="Submit" />
  19. </form>
  20. );
  21. }
  22. }
  • 因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。

  • 媒体播放:基于 React 的音乐或视频播放器可以利用 Refs 来管理其当前状态(播放/暂停),或管理播放进度等。这些更新不需要进行状态管理。
  • 触发强制动画:如果要在元素上触发过强制动画时,可以使用 Refs 来执行此操作。threejs
  • 集成第三方 DOM 库


  1. import React from 'react';
  2. // 不同于传递 createRef() 创建的 ref 属性,你会传递一个函数。
  3. // 这个函数中接受 React 组件实例或 HTML DOM 元素作为参数,以使它们能在其他地方被存储和访问。
  4. export default class Hello extends React.Component {
  5. constructor(props) {
  6. super(props);
  7. this.textRef = null; // 创建 ref 为 null
  8. }
  9. componentDidMount() {
  10. // 注意:这里没有使用 "current"
  11. // 直接使用原生 API 使 text 输入框获得焦点
  12. this.textRef.focus();
  13. }
  14. render() {
  15. // 把 <input> ref 关联到构造器里创建的 textRef 上
  16. return <input ref={node => this.textRef = node} />
  17. }
  18. }

React 将在组件挂载时将 DOM 元素传入ref 回调函数并调用,当卸载时传入 null 并调用它。在 componentDidMountcomponentDidUpdate 触发前,React 会保证 refs 一定是最新的。
像上例, ref 回调函数是以内联函数的方式定义的,在更新过程中它会被执行两次,第一次传入参数 null,然后第二次会传入参数 DOM 元素。
这是因为在每次渲染时会创建一个新的函数实例,所以 React 清空旧的 ref 并且设置新的。我们可以通过将 ref 的回调函数定义成 class 的绑定函数的方式可以避免上述问题,但是大多数情况下它是无关紧要的。

  1. export default class Hello extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. }
  5. componentDidMount() {
  6. // 通过 this.refs 调用
  7. // 直接使用原生 API 使 text 输入框获得焦点
  8. this.refs.textRef.focus();
  9. }
  10. render() {
  11. // 把 <input> ref 关联到构造器里创建的 textRef 上
  12. return <input ref='textRef' />
  13. }
  14. }

尽管字符串 stringRef 使用更方便,但是它有一些缺点,因此严格模式使用 stringRef 会报警告。官方推荐采用回调 Refs。

useRef()ref 属性更有用。useRef() Hook 不仅可以用于 DOM refs, useRef() 创建的 ref 对象是一个 current 属性可变且可以容纳任意值的通用容器,类似于一个 class 的实例属性。

  1. function Timer() {
  2. const intervalRef = useRef();
  3. useEffect(() => {
  4. const id = setInterval(() => {
  5. // ...
  6. });
  7. intervalRef.current = id;
  8. return () => {
  9. clearInterval(intervalRef.current);
  10. };
  11. });
  12. // ...
  13. }

这是因为它创建的是一个普通 Javascript 对象。而 useRef() 和自建一个 {current: …}对象的唯一区别是,useRef 会在每次渲染时返回同一个 ref 对象。

请记住,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

createRef 源码解析

  1. // ReactCreateRef.js 文件
  2. import type {RefObject} from 'shared/ReactTypes';
  3. // an immutable object with a single mutable value
  4. export function createRef(): RefObject {
  5. const refObject = {
  6. current: null,
  7. };
  8. if (__DEV__) {
  9. // 封闭对象,阻止添加新属性并将所有现有属性标记为不可配置。当前属性的值只要可写就可以改变。
  10. Object.seal(refObject);
  11. }
  12. return refObject;
  13. }
  14. ...
  15. export type RefObject = {
  16. current: any,
  17. };

forwardRef

React.createRef 中已经介绍过,有三种方式可以使用 React 元素的 ref
ref 是为了获取某个节点的实例,但是 函数式组件(PureComponent) 是没有实例的,不存在 this的,这种时候是拿不到函数式组件的 ref 的。

为了解决这个问题,由此引入 React.forwardRef**React.forwardRef** 允许某些组件接收 ref,并将其向下传递给 子组件

  1. const ForwardInput = React.forwardRef((props, ref) => (
  2. <input ref={ref} />
  3. ));
  4. class TestComponent extends React.Component {
  5. constructor(props) {
  6. super(props);
  7. this.inputRef = React.createRef(); // 创建 ref 存储 textRef DOM 元素
  8. }
  9. componentDidMount() {
  10. this.inputRef.current.value = 'forwardRef'
  11. }
  12. render() {
  13. return ( // 可以直接获取到 ForwardInput input 的 ref:
  14. <ForwardInput ref={this.inputRef}>
  15. )
  16. }
  17. }
  • 只在使用 React.forwardRef 定义组件时,第二个参数 ref 才存在
  • 在项目中组件库中尽量不要使用 React.forwardRef ,因为它可能会导致子组件被 破坏性更改
  • 函数组件 和 class 组件均不接收 **ref** 参数 ,即 props 中不存在 refref 必须独立 props 出来,否则会被 React 特殊处理掉。
  • 通常在 高阶组件(HOC) 中使用 **React.forwardRef**

**<br />

  1. function enhance(WrappedComponent) {
  2. class Enhance extends React.Component {
  3. componentWillReceiveProps(nextProps) {
  4. console.log('Current props: ', this.props);
  5. console.log('Next props: ', nextProps);
  6. }
  7. render() {
  8. const {forwardedRef, ...others} = this.props;
  9. // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
  10. return <WrappedComponent ref={forwardedRef} {...others} />;
  11. }
  12. }
  13. // 注意 React.forwardRef 回调的第二个参数 “ref”。
  14. // 我们可以将其作为常规 prop 属性传递给 Enhance,例如 “forwardedRef”
  15. // 然后它就可以被挂载到被 Enhance 包裹的子组件上。
  16. return React.forwardRef((props, ref) => {
  17. return <Enhance {...props} forwardedRef={ref} />;
  18. });
  19. }
  20. // 子组件
  21. class MyComponent extends React.Component {
  22. focus() {
  23. // ...
  24. }
  25. // ...
  26. }
  27. // EnhancedComponent 会渲染一个高阶组件 enhance(MyComponent)
  28. const EnhancedComponent = enhance(MyComponent);
  29. const ref = React.createRef();
  30. // 我们导入的 EnhancedComponent 组件是高阶组件(HOC)Enhance。
  31. // 通过React.forwardRef 将 ref 将指向了 Enhance 内部的 MyComponent 组件
  32. // 这意味着我们可以直接调用 ref.current.focus() 方法
  33. <EnhancedComponent
  34. label="Click Me"
  35. handleClick={handleClick}
  36. ref={ref}
  37. />;

forwardRef 与 Hook useImperativeHandle

useImperativeHandle 可以让你在使用 ref 时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle 应当与 forwardRef一起使用:

  1. function FancyInput(props, ref) {
  2. const inputRef = useRef();
  3. useImperativeHandle(ref, () => ({
  4. focus: () => {
  5. inputRef.current.focus();
  6. }
  7. }));
  8. return <input ref={inputRef} ... />;
  9. }
  10. FancyInput = forwardRef(FancyInput);

在本例中,渲染 <FancyInput ref={fancyInputRef} /> 的父组件可以调用 fancyInputRef.current.focus()

源码解读

  1. export default function forwardRef<Props, ElementType: React$ElementType>(
  2. render: (props: Props, ref: React$Ref<ElementType>) => React$Node,
  3. ) {
  4. if (__DEV__) {
  5. if (render != null && render.$$typeof === REACT_MEMO_TYPE) {
  6. warningWithoutStack(
  7. false,
  8. 'forwardRef requires a render function but received a `memo` ' +
  9. 'component. Instead of forwardRef(memo(...)), use ' +
  10. 'memo(forwardRef(...)).',
  11. );
  12. } else if (typeof render !== 'function') {
  13. warningWithoutStack(
  14. false,
  15. 'forwardRef requires a render function but was given %s.',
  16. render === null ? 'null' : typeof render,