React Hooks用法详解(二)

React Hooks

在了解React Hooks之前, 我们先看一下 Class函数式 的一般写法

Class组件 一般写法

import React from "react";
import ReactDOM from "react-dom";
import PropTypes from "prop-types";

class App extends React.Component {
  // 默认参数
  static defaultProps = {
    message: "Hello World",
    firstName: 'Yang',
    lastName: 'yu'
  };

  // 传参检查
  static propTypes = {
    message: PropTypes.string
  };

  /**
   * 方便调试
   */
  static displayName = "App";

  // 状态
  state = {
    count: 1
  };

  /**
   * 解决绑定 this 的几种方法
   * 1. onClick={this.increment.bind(this)}
   * 2. 在 constructor中绑定 tihs ==> this.increment = this.increment.bind(this)
   * 3 箭头函数: 浪费内存
   */
  increment = () => {
    this.setState({
      count: 2
    });
  };

  // 计算属性
  get name() {
    return this.props.firstName + this.props.lastName
  }

  // 生命周期
  componentDidMount() {}

  render() {
    return (
      <div>
        <div>{this.props.message}</div>
        <div>{this.state.count}</div>
        <button onClick={this.increment}>点击</button>
        <div>{this.name}</div>
      </div>
    );
  }
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

函数组件一般写法

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";

interface Props {
  message?: string;
}

const App: React.FunctionComponent<Props> = props => {
  /**
   * 如何使用 state,  React版本 > 16.8
   * useState 返回一个数组, 解构
   */
  const [count, setCount] = useState(0);
  const [count1, setCount1] = useState(0);

  const increment = () => {
    setCount(count + 1);
  };

  const increment1 = () => {
    setCount1(count1 + 1);
  };

  /**
   * 生命周期 代替 componetDidMount componentDidUpdate
   */
  useEffect(() => {
    console.log('componentDidMount() 或者 componentDidUpdate()')
  })

  // 第二个参数是 []
  useEffect(() => {
    console.log('只在第一次执行')
    // axios.get()
  }, [])
  
  // 当某个 参数更新, 才触发
  useEffect(() => {
    console.log('count 更新了之后执行, 点击 count1 不会更新')
  }, [count])

  useEffect(() => {
    return () => {
      console.log('我死了')
    }
  })

  return (
    <div>
      <div>{props.message}</div>
      <div>{count}</div>
      <div>{count1}</div>

      <button onClick={increment}>按钮</button>
      <button onClick={increment1}>按钮1</button>
    </div>
  );
};

App.defaultProps = {
  message: 'Hello World'
}
App.displayName = 'yym'

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);


下面我们来看看一些 Hooks API

useState

  • 使用

    const [n, setN] = React.useState(0)
    const [user, setUser] = React.useState({name: 'yym'})
    
  • 不可局部更新

    • 因为useState不会帮我们合并属性

      setUser({
          ...user,
          name: 'yym1'
      })
      
  • 地址会变

    • setState(obj) 如果 obj 地址不变, 那么 React 认为数据没有变化
  • 接收函数

    const [user, setUser] = useState(() => ({name: 'yym2', age: 12})) // 减少多余计算过程
    
    setN(i => i + 1)
    setN(i => i + 1)  // 对  state 多次操作,  
    

简单实现 useState

下面代码可以放到 codesandbox 里面跑一下

看一个简单的例子

const App = () => {
  const [n, setN] = useState(0);
  return (
    <div>
      <div>{n}</div>
      <button onClick={() => setN(n + 1)}>incrementN</button>
    </div>
  );
};
ReactDOM.render(<App />, document.querySelector("#root"))

1. 进入页面, render App, 调用 App() , n = 0
2. 用户点击 button, 调用 setN(n + 1), 再次 render App, 得到虚拟 DOM, DOM diff 更新真 DOM
3. 每次调用 App, 都会运行 useState(0), n 的值都会改变 


QA:
1. 执行 setN 的时候会发生什么? n 会变吗? App() 会重新执行吗?
  重新渲染; setN要把n改变, n 不变; App 会重新执行;
2. 如果 App() 会重新执行, 那么 useState(0) 的时候, n 每次值会不同
  n 的值会不同

上面的例子我们可以分析:

  • setN
    • setN 一定会修改数据 X, 将n + 1 存入 X, (X 泛指)
    • setN一定会触发<App />, 重新渲染
  • useState
    • useState 会从 X 读取 n 的最新值
  • X
    • 每个组件有自己的数据 X, 我们将其命名为state

根据上面的结论, 我们尝试写一下 useState

import React from "react";
import ReactDOM from "react-dom";

let _state: any;
const myUseState = (initialValue: any) => {
  _state = _state === undefined ? initialValue : _state;
  const setState = (newState: any) => {
    _state = newState;
    // 渲染页面
    render();
  };
  return [_state, setState];
};

// 不管这个实现
const render = () => ReactDOM.render(<App />, document.querySelector("#root"));

const App = () => {
  console.log("App 渲染了");
  
  // 使用自己写的 myUseState
  const [n, setN] = myUseState(0);
  return (
    <div>
      <div>{n}</div>
      <button onClick={() => setN(n + 1)}>incrementN</button>
    </div>
  );
};

export default App;

上面的代码完全可以正常运行, 但当我们的 myUseState 有两个的时候, 就是下面的代码

import React from "react";
import ReactDOM from "react-dom";

let _state: any;
const myUseState = (initialValue: any) => {
  _state = _state === undefined ? initialValue : _state;
  const setState = (newState: any) => {
    _state = newState;
    // 渲染页面
    render();
  };

  return [_state, setState];
};

// 不管这个实现
const render = () => ReactDOM.render(<App />, document.querySelector("#root" ));

const App = () => {
  console.log("App 渲染了");
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(1);
  return (
    <div>
      <div>
        {n}-{m}
      </div>
      <button onClick={() => setN(n + 1)}>incrementN</button>
      <button onClick={() => setM(m + 1)}>incrementM</button>
    </div>
  );
};

export default App;

我们运行上面的代码, 发现更新 n, m 也会改变, 更新m, n 也会改变, 因为所有的数据都放在 _state 会冲突, 该怎么解决?

  1. _state 做成对象

    _state = { m: 0, n: 0}, // 感觉不行,  useState(0) 并不知道变量叫 m 还是 n, 
    
  2. _state 做成数组, 尝试一下

    _state = [0, 0]
    
    // 把 _state 初始为数组
    let _state: any[] = [];
    // 下标
    let index = 0;
    
    const myUseState = (initialValue: any) => {
      // 设置唯一的值, 根据 myUseState 的顺序
      const currentIndex = index;
      _state[currentIndex] =
        _state[currentIndex] === undefined ? initialValue : _state[currentIndex];
      const setState = (newState: any) => {
        _state[currentIndex] = newState;
        // 渲染页面
        render();
      };
      index += 1;
      return [_state[currentIndex], setState];
    };
    
    const render = () => {
      // 每次重新渲染 index 重置
      index = 0;
      ReactDOM.render(<App />, document.querySelector("#root"));
    };
    
  3. 上面数组的方法会有一定的缺点

    • useState的调用顺序
      • 若第一次顺序是 n m K
      • 第二次必须保证顺序一致,
      • useState 不能写在 if
      const [n, setN] = React.useState(0);
      let m, setM;
      if (n % 2 === 1) {
        [m, setM] = React.useState(0);
      }
    
    // 会报错 顺序不能变
    React Hook "React.useState" is called conditionally. React Hooks must be called in the exact same order in every component render. (react-hooks/rules-of-hooks)eslint
    

代码在这里, 我们想一下 mpUseState 还有什么问题呢?

  1. App 组件用了 _stateindex , 那其它组件用什么?
    • 给每个组件创建一个_stateindex
  2. 放在全局作用域重名了怎么办?
    • 放在组件对应的虚拟节点对象上

总结 :

  1. 每个函数组件对应一个 React 节点,

  2. 每个节点保存着 _stateidnex

  3. useState 会读取 state[index]

  4. indexuseState 出现顺序决定

  5. setState 会修改 state, 并触发更新

上面的属于简化 useState 的实现, 我们看一个 useState 出现问题的 代码

// 先点击 log , 再点击 incremwntN,  3s 后打印的是 n: 0, 而不是 n: 1
// 先点击 incrementN, 再点击 log, 3s 后 打印 n: 1

// ? 为啥是旧数据
1. setN 不会改变 n, 生成一个 n 的分身, 改变的不是n, 所以 n: 0, 

import React, { useState } from "react";

const App = () => {
  const [n, setN] = useState(0);
  const log = () =>
    setTimeout(() => {
      console.log(`n: ${n}`);
    }, 3000);
  return (
    <div>
      <div>{n}</div>
      <button onClick={() => setN(n + 1)}>incrementN</button>
      <button onClick={log}>log</button>
    </div>
  );
};

export default App;

如何让状态始终只是一个?

  • 全局变量

    • window.xxx即可
  • useRef

    const App = () => {
      const nRef = React.useRef(0); // { current: 0}
      const log = () =>
        setTimeout(() => {
          console.log(`n: ${nRef.current}`);
        }, 3000);
      return (
        <div>
          <div>{nRef.current} 不是实时刷新</div>
          <button onClick={() => (nRef.current += 1)}>incrementN</button>
          <button onClick={log}>log</button>
        </div>
      );
    };
    
    
    // 手动触发 App 更新, 强制, 更类似于 Vue3
    const App = () => {
      const nRef = React.useRef(0); // { current: 0}
      const log = () =>
        setTimeout(() => {
          console.log(`n: ${nRef.current}`);
        }, 3000);
      return (
        <div>
          <div>{nRef.current}</div>
          <button onClick={() => (nRef.current += 1)}>incrementN</button>
          <button onClick={log}>log</button>
        </div>
      );
    };
    
  • useContext

    • useContext 不仅能贯穿始终, 还能贯穿不同组件

      // 上下文
      const themeContext = createContext(null);
      
      const App = () => {
        const [theme, setTheme] = useState("blue");
        return (
          // Provider 类似于作用域, 里面的可以使用 theme, settheme
          <themeContext.Provider value={{ theme, setTheme }}>
            <div style={{ backgroundColor: theme === "red" ? "red" : "blue" }}>
              <p>{theme}</p>
              <div>
                <ChildA />
              </div>
              <div>
                <ChildB />
              </div>
            </div>
          </themeContext.Provider>
        );
      };
      
      const ChildA = () => {
        const { setTheme } = useContext(themeContext);
        return (
          <div>
            <button onClick={() => setTheme("red")}>red</button>
          </div>
        );
      };
      
      const ChildB = () => {
        const { setTheme } = useContext(themeContext);
        return (
          <div>
            <button onClick={() => setTheme("blue")}>blue</button>
          </div>
        );
      };
      

useReducer

使用方法:

  • useReducer 是复杂点的 useState
import React, { useReducer } from "react";
// 1. 创建初始值
const initial = {
  n: 0
};
// 2. 创建所有操作 reducer(state, action)
const reducer = (state: any, action: any) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multiple") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown word");
  }
};

const App = () => {
  // 3. 传给 useReducer  得到读和写 API
  const [state, dispatch] = useReducer(reducer, initial);

  const onClick = () => {
    // 4. 写
    dispatch({ type: "add", number: 1 });
  };

  const onClick1 = () => {
    dispatch({ type: "add", number: 2 });
  };

  return (
    <div>
      {/* 4. 读取值 */}
      <h1>n: {state.n}</h1>

      <button onClick={onClick}>add+1</button>
      <button onClick={onClick1}>add+2</button>
    </div>
  );
};

export default App;

但如何代替 Redux

我们先弄一个初始的页面, 在里面一步步实现 Redux 功能, 示例Demo

// 这是我们初始页面, 有三个组件 User Books Movies
import React, { useReducer } from "react";
const App = () => {
  return (
    <div>
      <User />
      <hr />
      <Books />
      <Movies />
    </div>
  );
};
const User = () => {
  return (
    <div>
      <h1>个人信息</h1>
    </div>
  );
};
export default App;

  1. 先把数据集中到 store

    // 代码和初始一样, 只是往里面加代码
    import React, { useReducer } from "react";
    
    const store = {
      user: null,
      books: null,
      movies: null,
    }
    
  2. 创建 reducer, 使用 usereducer 一样的

    const reducer = (state, action) => {
      switch (action.type) {
        case "setUser":
          return { ...state, user: action.user };
        case "setBooks":
          return { ...state, books: action.books };
        case "setMovies":
          return { ...state, movies: action.movies };
        default:
          throw new Error();
      }
    };
    
  1. 我们使用 createContext, 创建一个上下文

    // 创建一个 context
    const Context = createContext(null);
    
  2. 使用useReducer 并把 useReducer 的读写 API, 放进 Context Value

    const App = () => {
      // 使用 useReducer
      const [state, dispatch] = useReducer(reducer, store);
      return (
        // 把 useReducer 的读写 API 放到 Context value里
        <Context.Provider value={{ state, dispatch }}>
          <User />
          <hr />
          <Books />
          <Movies />
        </Context.Provider>
      );
    };
    
    
  3. 经过前四步, 我们就可以在在 User, Books, Movie 的组件中使用 Context.Providevalue

    const User = () => {
      // 使用 useContext
      const { state, dispatch } = useContext(Context);
     
      // 异步请求值
      useEffect(() => {
        axios.get("/user").then((user: any) => {
          dispatch({ type: "setUser", user });
        });
      }, []);
    
      return (
        <div>
          <h1>个人信息</h1>
          <div>name: {state.user ? state.user.name : ""}</div>
        </div>
      );
    };
    
    
  4. 所以可以带到一个简单的 Redux 使用,

    // 所有代码
    import React, { useReducer, createContext, useContext, useEffect } from "react";
    
    // store
    const store = {
      user: null,
      books: null,
      movies: null
    };
    
    // reducer
    const reducer = (state, action) => {
      switch (action.type) {
        case "setUser":
          return { ...state, user: action.user };
        case "setBooks":
          return { ...state, books: action.books };
        case "setMovies":
          return { ...state, movies: action.movies };
        default:
          throw new Error();
      }
    };
    
    // 创建一个 context
    const Context = createContext(null);
    
    const App = () => {
      // 使用 useReducer
      const [state, dispatch] = useReducer(reducer, store);
      return (
        // 把 useReducer 的读写 API 放到 Context value里
        <Context.Provider value={{ state, dispatch }}>
          <User />
        </Context.Provider>
      );
    };
    
    // user 可以使用 store 的值
    const User = () => {
      const { state, dispatch } = useContext(Context);
    
      useEffect(() => {
        axios.get("/user").then((user: any) => {
          dispatch({ type: "setUser", user });
        });
      }, []);
    
      return (
        <div>
          <h1>个人信息</h1>
          <div>name: {state.user ? state.user.name : ""}</div>
        </div>
      );
    };
    
    export default App;
    
    

当一个项目比较大, 用到的组件比较多, 使用到的 reducer 比较多, 我们可以模块化

  • user, books, movies 单独建一个文件

  • 把对应的东西 export (default) 出去

    // 例: 重构 reducer
    // reducer
    const reducer = (state, action) => {
      switch (action.type) {
        case "setUser":
          return { ...state, user: action.user };
        case "setBooks":
          return { ...state, books: action.books };
        case "setMovies":
          return { ...state, movies: action.movies };
        default:
          throw new Error();
      }
    };
    
    // ==>
    
    const obj = {
      'setUser': (state, action) => {
        return { ...state, user: action.user };
      },
      // ...
    }
    
    const reducer_copy = (state, action) => {
      const fn = obj[action.type]
      if(fn) {
        return fn(state, action)
      } else {
        throw new Error('type 错误')
      }
    }
    
    
    // user_reducer.js
    export default {
      'setUser': (state, action) => {
        return { ...state, user: action.user };
      },
    }
    
    // books_reducer.js
    // ...
    
    // idnex.js
    import userReducer from './reducers/user_reducer'
    
    const obj = {
      ...userReducer,
      ...otherReducer
    }
    

useContext

useContext 改变一个数的时候, 是通过自顶向下, 逐级更新数据做到的

上下文

  • 全局变量是全局的上下文
  • 上下文是局部的全局变量

在上面的 useReducer 中我们也是用了 useContext,

import React, { createContext, useState, useContext } from "react";

// 1. 创建一个 context 上下文
const Context = createContext(null);

function App() {
  console.log("App 执行了");
  const [n, setN] = useState(0);
  return (
    // 2. 使用 Context.Provide 弄一个 作用域 value 值
    <Context.Provider value={{ n, setN }}>
       <div className="App">
         <Parent />
       </div>
    </Context.Provider>
  );
}

function Parent() {
  // 3. 作用域内使用 useContext
  const { n, setN } = useContext(Context);
  return (
    <div>
      我是爸爸 n: {n} <Child />
    </div>
  );
}

function Child() {
  const { n, setN } = useContext(Context);
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是儿子 我得到的 n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}

useEffect & useLayoutEffect

useEffect

  • 副作用: 对环境的改变就是副作用
  • 其实也可以当做afterRender: 每次render后运行
  1. 用途

    • 作为 componentDidMount 使用, []作为第二个参数
    • 作为componentDidUpdate使用, 可制定依赖
    • 作为componentWillUnMount 使用, 通过 return
    • 以上三种可以同时存在
  2. 特点

    • 如果同时存在多个 useEffect, 会按照出现次序执行
  3. Demo

    import React, { useState, useEffect } from "react";
    
    const App = () => {
      const [n, setN] = useState(0);
      const [m, setM] = useState(0);
      const onClick = () => {
        setN(n + 1);
      };
      const onClcik1 = () => {
        setM(m + 1);
      };
      
      // componentDidMount
      useEffect(() => {
        console.log("第一次渲染, n或m变化我也不打印了");
        document.title = "Hello";
      }, []);
    
      // componentDidUpdate
      useEffect(() => {
        console.log("第一二..N次渲染, 任何state变化我就渲染");
      });
      // componentDidUpdate
      useEffect(() => {
        console.log("第一次渲染, 并且只有m变化我再渲染");
      }, [m]);
     
      // componentDidMount & componentWillUnMount
      useEffect(() => {
        const timerId: any = setInterval(() => {
          console.log("定时器");
        }, 1000);
        return () => {
          window.clearInterval(timerId);
        };
      });
      return (
        <div>
          n: {n}
          <button onClick={onClick}>+n</button>
          <hr />
          m: {m}
          <button onClick={onClcik1}>+m</button>
        </div>
      );
    };
    
    export default App;
    
    

useLayoutEffect

  • 其函数签名与 useEffect 相同,但它会在所有的 DOM 变更之后同步调用 effect。可以使用它来读取 DOM 布局并同步触发重渲染。在浏览器执行绘制之前,useLayoutEffect 内部的更新计划将被同步刷新。
  • useEffect在浏览器渲染完成后执行
  • useLayout 在浏览器渲染前执行

需要截图 老是的白板

  1. 特点
    • useLayoutEffect总是比 useEffect 先执行 (上面说useEffect按次序执行, 如果有useLayoutEffect, 先执行)
    • useLayoutEffect 里的任务最好影响了 Layout
    • 尽可能使用标准的 useEffect 以避免阻塞视觉更新
    • 优先使用useEffect

useMemo & useCallback

  1. 在了解 React.useMemo 之前, 我们先来了解一下 React.memo, 直接看代码

    • 我们平常写React, 经常会有多余的render
    • React.memo 帮助我们控制何时重新渲染组件, 可以和函数式组件一起使用
    // 这是一个普通的父子组件, 子组件使用了父组件的 state
    const App = () => {
      const [n, setN] = useState(0);
      const [m, setM] = useState(0);
      const onClick = () => {
        setN(n + 1)
      }
      return (
        <div>
          <button onClick={onClick}>update: {n}</button>
          <Child data={m} />
        </div>
    };
    
    const Child = (props: any) => {
      console.log('child 执行了');
      // TODO: 大量代码
      return <div>child: {props.data}</div>
    }
    
    
    • 上面的代码我每次更新 n , 在控制台看到会更新 Child组件, 但是我们的 Child 组件, 只依赖m, 但 m 没有变化, 我们不希望Child 执行
    • props 不变, 没必要执行函数组件
    • 但我们怎么优化呢? React.memo
    // 上面的代码改变一下
    
    // 使用 React.memo 包裹一下, 只有当我们改变 m 的时候, Child 才会执行
    const Child = React.memo((props: any) => {
      console.log("child 执行了");
      // TODO: 大量代码
      return <div>child: {props.data}</div>;
    });
    
    • React.memo 使得一个组件只有在它的 props变化在执行一遍, 再次执行
  1. 但是React.memo 有一个问题: 添加监听函数, 还是会执行 Child, 看代码

    const App = () => {
      const [n, setN] = useState(0);
      // 每次执行 m = 0, 值是一样的
      const [m, setM] = useState(0);
      const onClick = () => {
        // n 变化不会执行 Child
        setN(n + 1);
      };
      
      // 每次App 执行, 都会重新执行
      // 之前是一个空函数, 现在是另一个空函数, 不是同一个空函数, 引用类型, 地址不同
      const onClickChild = () => {};
      return (
        <div>
          <button onClick={onClick}>update: {n}</button>
          {/* 传一个函数 */}
          <Child data={m} onClick={onClickChild} />
        </div>
      );
    };
    
    const Child = React.memo((props: any) => {
      console.log("child 执行了");
      // TODO: 大量代码
      // 在这里添加了 onClick事件
      return <div onClick={props.onClick}>child: {props.data}</div>;
    });
    
    • 怎么解决呢? react.useMemo
  1. React.useMemo

    • 如何使用?

      // render 时: 先根据[name]里面的 name判断, 因为 useMemo 作为一个有着暂存能力, 暂存了上一次的结果
      // 对比上一次 name, name值不变, data就不重新赋值成新的对象, 没有新的对象, 没有新的内存地址, 就不会
      // 重新渲染
      
      const data = useMemo(()=>{
        return {}
      },[m, n])
      
      // 1. 第一个参数 () => value
      // 2. 第二个参数依赖 [m, n]
      // 3. 当依赖变化, 重新计算新的 value
      // 4. 以来不变, 使用之前的 value
      
      //函数
      const data = useMemo(() => {
        return () => {}
      })
      
    • 上面使用 React.memo 改造成React.useMemo

      const App = () => {
        const [n, setN] = useState(0);
        const [m, setM] = useState(0);
        const onClick = () => {
          setN(n + 1);
        };
        
        // useMemo
        const onClickChild = useMemo(() => {
          return () => {};
        }, [m]);
        return (
          <div>
            <button onClick={onClick}>update: {n}</button>
            {/* dlkfs */}
            <Child data={m} onClick={onClickChild} />
          </div>
        );
      };
      
      const Child = React.memo((props: any) => {
        console.log("child 执行了");
        // TODO: 大量代码
        return <div onClick={props.onClick}>child: {props.data}</div>;
      });
      
  1. 有点难用, 于是有了 useCallback, 作用和 useMemo 一样, 是useMemo的语法糖

    • 用法

      useCallback( () => { callback }, [input],)
      
      // 等价于
      
      useMemo(() => () => { callback }, [input])
      
      const onClickChild = useMemo(() => {
        return () => {};
      }, [m]);
      
      const onClickChild = useCallback(() => {}, [m]);
      

useRef

  1. 使用

    目的: 
    1. 如果需要一个值, 在组建不断 render 时保持不变
    使用:
    1. 初始化 => const count = useRef(0)
    2. 读取: count.current
    
    const App = () => {
      const [n, setN] = useState(0);
      // 初始化一个值
      const count = useRef(0);
      const onClick = () => {
        setN(n + 1);
      };
    
      useEffect(() => {
        // 通过 .current 来读取, 更新一次 +1
        count.current += 1;
        console.log(count.current);
      });
      return (
        <div>
          <button onClick={onClick}>update: {n}</button>
        </div>
      );
    };
    
  2. 为什么需要current ?

    • 为了保证两次 useRef 是同一个值 (只有引用能做到)
  3. 做几个 Hook 对比

    • useState/useReducer 每次都变
    • useMemo/useCallback 依赖变化才变
    • useRef 永远不变
  4. 对比 Vue3 的 ref

    // vue
    <template>
     <div @click="increment">
        ref会更新: {count}
      </div>
    </template>
    <script>
      import  { ref } from 'vue'
     export default {
        setup() {  
          // 直接使用 ref
          const count = ref(0)
             const increment = () => {
            count.value++
          }
          return { count, increment}
        }
      }
    </script>
    
    // react 的 ref 不会自动更新ui 
    const App = () => {
      const [n, setN] = useState(0);
      const count = useRef(0);
      const onClick = () => {
        setN(n + 1);
      };
    
      const onClick2 = () => {
        count.current += 1;
        // 会改变值 +1
        console.log(count.current, "count...");
      };
    
      return (
        <div>
          <button onClick={onClick}>update: {n}</button>
          {/* 页面没有更新 */}
          <button onClick={onClick2}>update count: {count.current}</button>
        </div>
      );
    };
    
    // 在 实现 useState 中我们有如何手动刷新页面的方法, 使用 useState
    

forwardRef & useImperativeHandle

  1. 简单看个例子

    const App = () => {
      const buttonRef = useRef(null);
      return (
        <div>
          {/* 我们想要获取子组件的 DOM 引用 */}
          <ButtonComponent ref={buttonRef}>按钮</ButtonComponent>
        </div>
      );
    };
    
    const ButtonComponent = (props: any) => {
      console.log(props, 'props....')  // {children: "按钮"} 没有得到 ref
      return <button className="red" {...props} />;
    };
    
    // warning:  Function components cannot be given refs. Attempts to access this ref will fail. Did you mean to use React.forwardRef()?
    
    // 上面的报错, 函数组件不支持 ref, 可以使用 React.forwardRef()
    
  1. 使用forwardRef

    // 函数组件希望接收别的传来的 ref, 需要 forwardRef 包起来
    // forwardRef 只是多加了一个 ref 参数
    // 改造上面的
    const App = () => {
      const buttonRef = useRef(null);
      return (
        <div>
          <Button2 ref={buttonRef}>按钮</Button2>
        </div>
      );
    };
    
    // forwardRed 包裹
    const Button2 = forwardRef((props, ref) => {
      return <button className="red" ref={ref} {...props} />;
    });
    
  1. 总结:

    useRef: 
    1. 可以用来引用 DOM 对象
    2. 也可以用来引用普通对象
    
    forwardRef
    1. 函数式组件 由于 props 不包含 ref, 所以需要 forwardRef
    
  1. forwardRef 相关的 useImperativeHandle, 减少暴露给父组件的属性

    // 官方例子:
    function FancyInput(props, ref) {
      const inputRef = useRef();
      
      // 可以自定义暴露出去的实例值
      // 其实可以叫做 setRef
      useImperativeHandle(ref, () => ({
        focus: () => {
          inputRef.current.focus();
        }
      }));
      return <input ref={inputRef} ... />;
    }
    FancyInput = forwardRef(FancyInput);
    

自定义 Hook

  • 自定义 Hook 是一个函数,其名称以 “use” 开头,函数内部可以调用其他的 Hook
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,393评论 5 467
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,790评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,391评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,703评论 1 270
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,613评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,003评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,507评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,158评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,300评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,256评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,274评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,984评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,569评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,662评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,899评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,268评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,840评论 2 339