1. React Hooks #

2. 解决的问题 #

3. 注意事项 #

4. useState #

const [state, setState] = useState(initialState);

4.1 计数器 #

import React,{useState} from 'react';
class Counter extends React.Component {
  constructor(props) {
      super(props);
      this.state = {
          number: 0
      };
  }
  render() {
      return (
          <div>
              <p>{this.state.number}</p>
              <button onClick={() => this.setState({ number: this.state.number + 1 })}>
                  +
        </button>
          </div>
      );
  }
}
function Counter2(){
  const [number,setNumber] = useState(0);
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
      </>
  )
}
export default Counter2;

4.2 每次渲染都是独立的闭包 #

function Counter2(){
  const [number,setNumber] = useState(0);
  function alertNumber(){
    setTimeout(()=>{
      alert(number);
    },3000);
  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}
function Counter() {
    const [number, setNumber] = useState(0);
    const savedCallback = useRef();
    function alertNumber() {
        setTimeout(() => {
            alert(savedCallback.current);
        }, 3000);
    }
    return (
        <>
            <p>{number}</p>
            <button onClick={() => {
                setNumber(number + 1);
                savedCallback.current = number + 1;
            }}>+</button>
            <button onClick={alertNumber}>alertNumber</button>
        </>
    )
}

4.3 函数式更新 #

function Counter2(){
  const [number,setNumber] = useState(0);
  let numberRef = useRef(number);
  numberRef.current = number;
  function alertNumber(){
    setTimeout(()=>{
      alert(numberRef.current);
    },3000);
  }
+  function lazy(){
+    setTimeout(()=>{
+      setNumber(number+1);
+    },3000);
+  }
+  function lazyFunc(){
+    setTimeout(()=>{
+      setNumber(number=>number+1);
+    },3000);
+  }
  return (
      <>
          <p>{number}</p>
          <button onClick={()=>setNumber(number+1)}>+</button>
          <button onClick={lazy}>lazy+</button>
          <button onClick={lazyFunc}>lazyFunc+</button>
          <button onClick={alertNumber}>alertNumber</button>
      </>
  )
}

4.4 惰性初始 state #

function Counter(){
  const [{name,number},setValue] = useState(()=>{
    return {name:'计数器',number:0};
  });
  return (
      <>
          <p>{name}:{number}</p>
          <button onClick={()=>setValue({number:number+1})}>+</button>
      </>
  )
}

4.5 性能优化 #

4.5.1 Object.is #

4.5.2 减少渲染次数 #

function Child({onButtonClick,data}){
  console.log('Child render');
  return (
    <button onClick={onButtonClick} >{data.number}</button>
  )
}
Child = memo(Child);
function App(){
  const [number,setNumber] = useState(0);
  const [name,setName] = useState('zhufeng');
  const addClick = useCallback(()=>setNumber(number+1),[number]);
  const  data = useMemo(()=>({number}),[number]);
  return (
    <div>
      <input type="text" value={name} onChange={e=>setName(e.target.value)}/>
      <Child onButtonClick={addClick} data={data}/>
    </div>
  )
}

4.6 useState+useCallback+useMemo实现 #

import React from 'react';
import ReactDOM from 'react-dom';
let hookStates = [];
let hookIndex = 0;
function useState(initialState){
  hookStates[hookIndex] = hookStates[hookIndex]||initialState;
  let currentIndex = hookIndex; 
  function setState(newState){
    hookStates[currentIndex]=newState;
    render();
  }
  return [hookStates[hookIndex++],setState];
}
function useMemo(factory,deps){
  if(hookStates[hookIndex]){
    let [lastMemo,lastDeps] = hookStates[hookIndex];
    let same = deps.every((item,index)=>item === lastDeps[index]);
    if(same){
      hookIndex++;
      return lastMemo;
    }else{
      let newMemo = factory();
      hookStates[hookIndex++]=[newMemo,deps];
      return newMemo;
    }
  }else{//如果取不到,说明第一次调用
    let newMemo = factory();
    hookStates[hookIndex++]=[newMemo,deps];
    return newMemo;
  }
}
function useCallback(callback,deps){
  if(hookStates[hookIndex]){
    let [lastCallback,lastDeps] = hookStates[hookIndex];
    let same = deps.every((item,index)=>item === lastDeps[index]);
    if(same){
      hookIndex++;
      return lastCallback;
    }else{
      hookStates[hookIndex++]=[callback,deps];
      return callback;
    }
  }else{//如果取不到,说明第一次调用
    hookStates[hookIndex++]=[callback,deps];
    return callback;
  }
}

let  Child = ({data,handleClick})=>{
  console.log('Child render');
  return (
     <button onClick={handleClick}>{data.number}</button>
  )
}
class PureComponent extends React.Component {
  shouldComponentUpdate(newProps,nextState) {
    return !shallowEqual(this.props, newProps)||!shallowEqual(this.state, nextState);
  }
}
function shallowEqual(obj1, obj2) {
  if (obj1 === obj2) {
    return true;
  }
  if (typeof obj1 != "object" ||obj1 === null ||typeof obj2 != "object" ||obj2 === null) {
    return false;
  }
  let keys1 = Object.keys(obj1);
  let keys2 = Object.keys(obj2);
  if (keys1.length !== keys2.length) {
    return false;
  }
  for (let key of keys1) {
    if (!obj2.hasOwnProperty(key) || obj1[key] !== obj2[key]) {
      return false;
    }
  }
  return true;
}
function memo(OldComponent){
  return class extends PureComponent{
    render(){
      return <OldComponent {...this.props}/>
    }
  }
}
Child = memo(Child);

function App(){
  console.log('App render');
  const[name,setName]=useState('zhufeng');
  const[number,setNumber]=useState(0);
  let data = useMemo(()=>({number}),[number]);
  let handleClick = useCallback(()=> setNumber(number+1),[number]);
  return (
    <div>
      <input type="text" value={name} onChange={event=>setName(event.target.value)}/>
      <Child data={data} handleClick={handleClick}/>
    </div>
  )
}

function render(){
  hookIndex=0;
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}
render();

4.7 注意事项 #

5. useReducer #

5.1 基本用法 #

const [state, dispatch] = useReducer(reducer, initialArg, init);
import React from 'react';
import ReactDOM from 'react-dom';
function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {number: state.number + 1};
    case 'minus':
      return {number: state.number - 1};
    default:
      return state;
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter1(){
  const [state, setState] = React.useState({number:0});
  return (
      <>
        Count: {state.number}
        <button onClick={() => setState({number:state.number+1})}>+</button>
        <button onClick={() => setState({number:state.number-1})}>-</button>
      </>
  )
}
function Counter2(){
    const [state, dispatch] = React.useReducer(reducer, 0,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'add'})}>+</button>
          <button onClick={() => dispatch({type: 'minus'})}>-</button>
        </>
    )
}

function render(){
  ReactDOM.render(
    <>
     <Counter1 />
     <Counter2 />
    </>,
    document.getElementById('root')
  );
}
render();

实现

import React from 'react';
import ReactDOM from 'react-dom';
let hookStates = [];
let hookIndex = 0;
function useReducer(reducer, initialState,init) {
  hookStates[hookIndex]=hookStates[hookIndex]||(init?init(initialState):initialState);
  let currentIndex = hookIndex;
  function dispatch(action) {
    hookStates[currentIndex]=reducer?reducer(hookStates[currentIndex],action):action;
    render();
  }
  return [hookStates[hookIndex++], dispatch];
}
function useState(initialState){
  return useReducer(null,initialState);
}
function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {number: state.number + 1};
    case 'minus':
      return {number: state.number - 1};
    default:
      return state;
  }
}
function init(initialState){
    return {number:initialState};
}
function Counter1(){
  const [state, setState] = useState({number:0});
  return (
      <>
        Count: {state.number}
        <button onClick={() => setState({number:state.number+1})}>+</button>
        <button onClick={() => setState({number:state.number-1})}>-</button>
      </>
  )
}
function Counter2(){
    const [state, dispatch] = useReducer(reducer, 0,init);
    return (
        <>
          Count: {state.number}
          <button onClick={() => dispatch({type: 'add'})}>+</button>
          <button onClick={() => dispatch({type: 'minus'})}>-</button>
        </>
    )
}

function render(){
  hookIndex=0;
  ReactDOM.render(
    <>
     <Counter1 />
     <Counter2 />
    </>,
    document.getElementById('root')
  );
}
render();



/*
useMemo<{
  number: number;
}>(factory: () => {
  number: number;
}, deps: React.DependencyList): {
  number: number;
} */

6. useContext #


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

const CounterContext = React.createContext();

function reducer(state, action) {
  switch (action.type) {
    case 'add':
      return {number: state.number + 1};
    case 'minus':
      return {number: state.number - 1};
    default:
      return state;
  }
}
function Counter(){
  let {state,dispatch} = React.useContext(CounterContext);
  return (
      <>
        <p>{state.number}</p>
        <button onClick={() => dispatch({type: 'add'})}>+</button>
        <button onClick={() => dispatch({type: 'minus'})}>-</button>
      </>
  )
}
function App(){
    const [state, dispatch] = React.useReducer(reducer, {number:0});
    return (
        <CounterContext.Provider value={{state,dispatch}}>
          <Counter/>
        </CounterContext.Provider>
    )
}

ReactDOM.render(<App/>,document.getElementById('root'));
function useContext(context){
  return context._currentValue;
}

7. useEffect #

useEffect(didUpdate);

7.1 通过class实现修改标题 #

class Counter extends React.Component {
    constructor(props) {
      super(props);
      this.state = {
        number: 0
      };
    }

    componentDidMount() {
        document.title = `你点击了${this.state.number}次`;
    }

    componentDidUpdate() {
        document.title = `你点击了${this.state.number}次`;
    }

    render() {
      return (
        <div>
          <p>{this.state.number}</p>
          <button onClick={() => this.setState({ number: this.state.number + 1 })}>
            +
          </button>
        </div>
      );
    }
  }

在这个 class 中,我们需要在两个生命周期函数中编写重复的代码,这是因为很多情况下,我们希望在组件加载和更新时执行同样的操作。我们希望它在每次渲染之后执行,但 React 的 class 组件没有提供这样的方法。即使我们提取出一个方法,我们还是要在两个地方调用它。useEffect会在第一次渲染之后和每次更新之后都会执行

7.2 通过effect实现 #

import React,{Component,useState,useEffect} from 'react';
import ReactDOM from 'react-dom';
function Counter(){
    const [number,setNumber] = useState(0);
    // 相当于 componentDidMount 和 componentDidUpdate:
    useEffect(() => {
        // 使用浏览器的 API 更新页面标题
        document.title = `你点击了${number}次`;
    });
    return (
        <>
            <p>{number}</p>
            <button onClick={()=>setNumber(number+1)}>+</button>
        </>
    )
}
ReactDOM.render(<Counter />, document.getElementById('root'));

每次我们重新渲染,都会生成新的 effect,替换掉之前的。某种意义上讲,effect 更像是渲染结果的一部分 —— 每个 effect 属于一次特定的渲染。

7.3 跳过 Effect 进行性能优化 #

function Counter(){
  const [number,setNumber] = useState(0);
  // 相当于componentDidMount 和 componentDidUpdate
  useEffect(() => {
     console.log('开启一个新的定时器')
     const $timer = setInterval(()=>{
      setNumber(number=>number+1);
     },1000);
  },[]);
  return (
      <>
          <p>{number}</p>
      </>
  )
}

7.4 清除副作用 #

import React, { useEffect, useState, useReducer } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
    const [number, setNumber] = useState(0);
    useEffect(() => {
        console.log('开启一个新的定时器')
        const $timer = setInterval(() => {
            setNumber(number => number + 1);
        }, 1000);
        return () => {
            console.log('销毁老的定时器');
            clearInterval($timer);
        }
    });
    return (
        <>
            <p>{number}</p>
        </>
    )
}
function App() {
    let [visible, setVisible] = useState(true);
    return (
        <div>
            {visible && <Counter />}
            <button onClick={() => setVisible(false)}>stop</button>
        </div>
    )
}
ReactDOM.render(<App />, document.getElementById('root'));
import React,{useEffect,useState} from 'react';
import ReactDOM from 'react-dom';
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const id = setInterval(() => {
      console.log('setInterval',count);
      setCount(count + 1);
    }, 1000);
    return () => {
      console.log('clearInterval');
      clearInterval(id);
    };
  }, [count]);

  return <h1>{count}</h1>;
}

7.6 竞态 #

import React, { useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
const API = {
  async fetchArticle(id){
    return new Promise((resolve)=>{
        setTimeout(()=>{
          resolve({id,title:`title_${id}`});
        },1000*(5-id));
    });
  }
}
function Article({ id }) {
  const [article, setArticle] = useState({});
  useEffect(() => {
    let didCancel = false;
    async function fetchData() {
      const article = await API.fetchArticle(id);
      if (!didCancel) {
        setArticle(article);
      }
    }
    fetchData();
    return () => {
      didCancel = true;
    };
  }, [id]);
  return (
    <div>
      <p>{article.title}</p>
    </div>
  )
}
function App(){
  let [id,setId] = useState(1);
  return (
    <div>
      <p>id:{id}</p>
       <Article id={id}/>
       <button onClick={()=>setId(id+1)}>改变id</button>
    </div>
  )
}
function render() {
  ReactDOM.render(
    <App />,
    document.getElementById('root')
  );
}
render();

7.7 effect回调里读取最新的值 #

index.js

import React,{useEffect,useRef,useState} from 'react';
import ReactDOM from 'react-dom';
function Counter() {
  const [count, setCount] = useState(0);
  const latestCount = useRef(count);

  useEffect(() => {
    latestCount.current = count;
    setTimeout(() => {
      console.log(`You clicked ${latestCount.current} times`);
    }, 3000);
  });
  return (
    <div>
      <p>{count}</p>
      <button onClick={()=>setCount(count+1)}>+</button>
    </div>
  )
}
function render(){
  ReactDOM.render(
    <Counter/>,
    document.getElementById('root')
  );
}
render();

7.8 useEffect 实现 #

+let hookStates = [];
+let hookIndex = 0;
+function useState(initialState){
+  hookStates[hookIndex]=hookStates[hookIndex]||initialState;
+  let currentIndex = hookIndex;
+  function setState(newState){
+    hookStates[currentIndex]=newState;
+    render();
+  }
+  return [hookStates[hookIndex++],setState];
+} 

+function useEffect(callback,dependencies){
+  if(hookStates[hookIndex]){
+      let lastDeps = hookStates[hookIndex];
+      let same = dependencies.every((item,index)=>item === lastDeps[index]);
+      if(same){
+        hookIndex++;
+      }else{
+        hookStates[hookIndex++]=dependencies;
+        setTimeout(callback);
+      }
+  }else{
+     hookStates[hookIndex++]=dependencies;
+      setTimeout(callback);
+  }
+}

9. useRef #

const refContainer = useRef(initialValue);

9.1 使用useRef #

import React, { useState, useRef } from 'react';
import ReactDOM from 'react-dom';
function Parent() {
    let [number, setNumber] = useState(0);
    return (
        <>
            <Child />
            <button onClick={() => setNumber({ number: number + 1 })}>+</button>
        </>
    )
}
let input;
function Child() {
    const inputRef = useRef();
    console.log('input===inputRef', input === inputRef);
    input = inputRef;
    function getFocus() {
        inputRef.current.focus();
    }
    return (
        <>
            <input type="text" ref={inputRef} />
            <button onClick={getFocus}>获得焦点</button>
        </>
    )
}
ReactDOM.render(<Parent />, document.getElementById('root'));

9.2 forwardRef #

function Child(props,ref){
  return (
    <input type="text" ref={ref}/>
  )
}
Child = forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

9.3 useImperativeHandle #

function Child(props,ref){
  const inputRef = useRef();
  useImperativeHandle(ref,()=>(
    {
      focus(){
        inputRef.current.focus();
      }
    }
  ));
  return (
    <input type="text" ref={inputRef}/>
  )
}
Child = forwardRef(Child);
function Parent(){
  let [number,setNumber] = useState(0); 
  const inputRef = useRef();
  function getFocus(){
    console.log(inputRef.current);
    inputRef.current.value = 'focus';
    inputRef.current.focus();
  }
  return (
      <>
        <Child ref={inputRef}/>
        <button onClick={()=>setNumber({number:number+1})}>+</button>
        <button onClick={getFocus}>获得焦点</button>
      </>
  )
}

9.4 实现 #

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

let hookStates = [];
let hookIndex = 0;
function useState(initialState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initialState;
  let currentIndex = hookIndex;
  function setState(newState) {
    hookStates[currentIndex] = newState;
    render();
  }
  return [hookStates[hookIndex++], setState];
}
function useRef(initialState) {
  hookStates[hookIndex] =  hookStates[hookIndex] || { current: initialState };
  return hookStates[hookIndex++];
}

let lastInput;
function App() {
  let [number, setNumber] = useState(0);
  const inputRef = useRef();
  console.log('input===inputRef', lastInput === inputRef);
  lastInput = inputRef;
  return (
    <>
      <input type="text" ref={inputRef} />
      <button onClick={()=>setNumber(number+1)}>{number}</button>
    </>
  )
}
function render() {
  hookIndex = 0;
  ReactDOM.render(<App />, document.getElementById('root'));
}
render();

10. useLayoutEffect #

10.1 事件循环 #

10.2 使用 #

import React, {useRef } from 'react';
import ReactDOM from 'react-dom';
let lastDependencies;
function useEffect(callback,dependencies){
    if(lastDependencies){
        let changed = !dependencies.every((item,index)=>item==lastDependencies[index]);
        if(changed){
            setTimeout(callback)
            lastDependencies=dependencies;
        }
    }else{
        setTimeout(callback)
        lastDependencies=dependencies;
    }
}

let lastLayoutDependencies;
function useLayoutEffect(callback,dependencies){
    if(lastLayoutDependencies){
        let changed = !dependencies.every((item,index)=>item==lastLayoutDependencies[index]);
        if(changed){
            queueMicrotask(callback);
            lastLayoutDependencies=dependencies;
        }
    }else{
        Promise.resolve().then(callback);
        lastLayoutDependencies=dependencies;
    }
}
const Animate = ()=>{
    const ref = useRef();
    useLayoutEffect(()=>{
        ref.current.style.WebkitTransform = `translate(500px)`;
        ref.current.style.transition  = `all 500ms`;
    });
    let style = {
        width:'100px',
        height:'100px',
        backgroundColor:'red'
    }
    return (
        <div>
            <div style={style} ref={ref}></div>
        </div>
    )
}
function render(){
    ReactDOM.render(<Animate/>,document.getElementById('root'));
}
render();

10.3 useEffect 实现 #

function useLayoutEffect(callback,dependencies){
  if(hookStates[hookIndex]){
      let lastDeps = hookStates[hookIndex];
      let same = dependencies.every((item,index)=>item === lastDeps[index]);
      if(same){
        hookIndex++;
      }else{
        hookStates[hookIndex++]=dependencies;
+       queueMicrotask(callback);
      }
  }else{
     hookStates[hookIndex++]=dependencies;
     queueMicrotask(callback);
  }
}

9. 自定义 Hook #

9.1.自定义计数器 #

function useNumber(){
  const [number,setNumber] = useState(0);
  useEffect(() => {
     console.log('开启一个新的定时器')
     const $timer = setInterval(()=>{
      setNumber(number+1);
     },1000);
     return ()=>{
      console.log('销毁老的定时器')
         clearInterval($timer);
     }
  });
  return number;
}
function Counter1(){
  let number1 = useNumber();
  return (
      <>
          <p>{number1}</p>
      </>
  )
}
function Counter2(){
  let number = useNumber();
  return (
      <>
          <p>{number}</p>
      </>
  )
}
function App(){
  return <><Counter1/><Counter2/></>
}

9.2 ajax #

import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
function useRequest(url) {
    let limit = 5;
    let [offset, setOffset] = useState(0);
    let [data, setData] = useState([]);
    function loadMore() {
        setData(null);
        fetch(`${url}?offset=${offset}&limit=${limit}`)
            .then(response => response.json())
            .then(pageData => {
                setData([...data, ...pageData]);
                setOffset(offset + pageData.length);
            });
    }
    useEffect(loadMore, []);
    return [data, loadMore];
}

function App() {
    const [users, loadMore] = useRequest('http://localhost:8000/api/users');
    if (users === null) {
        return <div>正在加载中....</div>
    }
    return (
        <>
            <ul>
                {
                    users.map((item, index) => <li key={index}>{item.id}:{item.name}</li>)
                }
            </ul>
            <button onClick={loadMore}>加载更多</button>
        </>
    )
}
ReactDOM.render(<App />, document.getElementById('root'));

async+await

import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
function useRequest(url) {
    let limit = 5;
    let [offset, setOffset] = useState(0);
    let [data, setData] = useState([]);
    async function loadMore() {
        setData(null);
        let pageData = await fetch(`${url}?offset=${offset}&limit=${limit}`)
            .then(response => response.json());
        setData([...data, ...pageData]);
        setOffset(offset + pageData.length);
    }
    useEffect(loadMore, []);
    return [data, loadMore];
}

function App() {
    const [users, loadMore] = useRequest('http://localhost:8000/api/users');
    if (users === null) {
        return <div>正在加载中....</div>
    }
    return (
        <>
            <ul>
                {
                    users.map((item, index) => <li key={index}>{item.id}:{item.name}</li>)
                }
            </ul>
            <button onClick={loadMore}>加载更多</button>
        </>
    )
}
ReactDOM.render(<App />, document.getElementById('root'));
let express = require('express');
let app = express();
app.use(function (req, res, next) {
    res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
    next();
});
app.get('/api/users', function (req, res) {
    let offset = parseInt(req.query.offset);
    let limit = parseInt(req.query.limit);
    let result = [];
    for (let i = offset; i < offset + limit; i++) {
        result.push({ id: i + 1, name: 'name' + (i + 1) });
    }
    res.json(result);
});
app.listen(8000);

9.3 动画 #

import React, { useState, useEffect, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom';
import './index.css';
function useMove(initialClassName) {
    const [className, setClassName] = useState(initialClassName);
    const [state, setState] = useState('');
    function start() {
        setState('bigger');
    }
    useEffect(() => {
        if (state === 'bigger') {
            setClassName(`${initialClassName} ${initialClassName}-bigger`);
        }
    }, [state]);
    return [className, start];
}

function App() {
    const [className, start] = useMove('circle');
    return (
        <div>
            <button onClick={start}>start</button>
            <div className={className}></div>
        </div>
    )
}
ReactDOM.render(<App />, document.getElementById('root'));
.circle {
    width : 50px;
    height : 50px;
    border-radius: 50%;
    background : red;
    transition: all .5s;
  }
.circle-bigger {
    width : 200px;
    height : 200px;
}