React Hook详解

Rinsann 2021年10月17日 405次浏览

React Hook详解

useState

使用状态

  • const [n,setN] = React.useState(0)
  • const [user,setUser] = React.useState({name:’F’})

注意事项1:不可局部更新

  • 如果state是一个对象,能否部分setState?答案是不行。
  • 因为setState不会自动合并属性,useReducer也不会合并属性。

注意事项2:地址要变

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

useState接收函数

const [state,setState] = useState(()=>{
	return initialState
})

该函数返回初识state,且只执行一次

setState 接收函数

  • set(i => i+1),什么时候用这种方式
import React, {useState} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const onClick = ()=>{
    setN(n+1)
    setN(n+1) // 你会发现 n 不能加 2
    // setN(i=>i+1)  //应优先使用此方式
    // setN(i=>i+1)
  }
  return (
    <div className="App">
      <h1>n: {n}</h1>
       
      <button onClick={onClick}>+2</button>
    </div>
  );
}

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

useReducer

用来践行 Flux/Redux 思想

  1. 创建初始值initialState
  2. 创建所有操作reducer(state,action)
  3. 传给useReducer,得到读和写API
  4. 调用写({type:’操作类型‘})

总的来说useReducer 是 useState的复杂版

用useReducer的表单例子

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

const initFormData = {
  name: "",
  age: 18,
  nationality: "汉族"
};

function reducer(state, action) {
  switch (action.type) {
    case "patch": //更新
      return { ...state, ...action.formData };
    case "reset": //重置
      return initFormData;
    default:
      throw new Error('type不正确');
  }
}

function App() {
  const [formData, dispatch] = useReducer(reducer, initFormData);
  // const patch = (key, value)=>{
  //   dispatch({ type: "patch", formData: { [key]: value } })
  // }
  const onSubmit = () => {};
  const onReset = () => {
    dispatch({ type: "reset" });
  };
  return (
    <form onSubmit={onSubmit} onReset={onReset}>
      <div>
        <label>
          姓名
          <input
            value={formData.name}
            onChange={e =>
              dispatch({ type: "patch", formData: { name: e.target.value } })
            }
          />
        </label>
      </div>
      <div>
        <label>
          年龄
          <input
            value={formData.age}
            onChange={e =>
              dispatch({ type: "patch", formData: { age: e.target.value } })
            }
          />
        </label>
      </div>
      <div>
        <label>
          民族
          <input
            value={formData.nationality}
            onChange={e =>
              dispatch({
                type: "patch",
                formData: { nationality: e.target.value }
              })
            }
          />
        </label>
      </div>
      <div>
        <button type="submit">提交</button>
        <button type="reset">重置</button>
      </div>
      <hr />
      {JSON.stringify(formData)}
    </form>
  );
}

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

如何代替Redu

步骤

  1. 将数据集中在一个store对象
  2. 将所有操作集中在reducer
  3. 创建一个context
  4. 创建对数据的读写API
  5. 将第四步的内容放到第三步的Context
  6. 用Context.Provider 将Context提供给所有组件
  7. 各个组件用 useContext获取读写API
import React, { useReducer, useContext, useEffect } from "react";
import ReactDOM from "react-dom";

const store = {
  user: null,
  books: null,
  movies: null
};

function 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 Context = React.createContext(null);

function App() {
  const [state, dispatch] = useReducer(reducer, store);

  const api = { state, dispatch };
  return (
    <Context.Provider value={api}>
      <User />
      <hr />
      <Books />
      <Movies />
    </Context.Provider>
  );
}

function User() {
  const { state, dispatch } = useContext(Context);
  useEffect(() => {
    ajax("/user").then(user => {
      dispatch({ type: "setUser", user: user });
    });
  }, []);
  return (
    <div>
      <h1>个人信息</h1>
      <div>name: {state.user ? state.user.name : ""}</div>
    </div>
  );
}

function Books() {
  const { state, dispatch } = useContext(Context);
  useEffect(() => {
    ajax("/books").then(books => {
      dispatch({ type: "setBooks", books: books });
    });
  }, []);
  return (
    <div>
      <h1>我的书籍</h1>
      <ol>
        {state.books ? state.books.map(book => <li key={book.id}>{book.name}</li>) : "加载中"}
      </ol>
    </div>
  );
}

function Movies() {
  const { state, dispatch } = useContext(Context);
  useEffect(() => {
    ajax("/movies").then(movies => {
      dispatch({ type: "setMovies", movies: movies });
    });
  }, []);
  return (
    <div>
      <h1>我的电影</h1>
      <ol>
        {state.movies
          ? state.movies.map(movie => <li key={movie.id}>{movie.name}</li>)
          : "加载中"}
      </ol>
    </div>
  );
}

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

// 帮助函数

// 假 ajax
// 两秒钟后,根据 path 返回一个对象,必定成功不会失败
function ajax(path) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (path === "/user") {
        resolve({
          id: 1,
          name: "Frank"
        });
      } else if (path === "/books") {
        resolve([
          {
            id: 1,
            name: "JavaScript 高级程序设计"
          },
          {
            id: 2,
            name: "JavaScript 精粹"
          }
        ]);
      } else if (path === "/movies") {
        resolve([
          {
            id: 1,
            name: "爱在黎明破晓前"
          },
          {
            id: 2,
            name: "恋恋笔记本"
          }
        ]);
      }
    }, 2000);
  });
}

useContext

上下文

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

使用方法

  1. 使用C=createContext(initial) 创建上下文
  2. 使用 (C.provider)圈定作用域
  3. 在作用域内使用useContext(C)来使用上下文

注意事项

  • useContext不是响应式的,你再一个模块将C里面的值改变,另一个模块不会感知到这个变化

useEffect

副作用

  • 对环境的改变即为副作用,如修改document.title
  • 但是不一定要把副作用放在useEffect里面(每次render之后执行)

用途

  • 作为componentDidMount使用,[]作第二个参数
  • 作为componentDidUpdate使用,可指定依赖
  • 作为componentWillUnmount使用,通过return,以上三种用途可同时存在

特点

  • 如果存在多个useEffect,会按照出现次序执行

useLayoutEffect

布局副作用

  • useEffect在浏览器渲染完成后执行
  • useLayoutEffect在浏览器渲染前执行
import React, {useState, useRef, useLayoutEffect, useEffect} from "react";
import ReactDOM from "react-dom";

function App() {
  const [n, setN] = useState(0)
  const time = useRef(null)
  const onClick = ()=>{
    setN(i=>i+1) 
    time.current = performance.now()
  }
  useLayoutEffect(()=>{ // 改成 useEffect 试试
    if(time.current){
      console.log(performance.now() - time.current)
    }
  })
  return (
    <div className="App">
      <h1>n: {n}</h1>
      <button onClick={onClick}>Click</button>
    </div>
  );
}

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

特点

  • useLayoutEffect 总是比 useEffect先执行
  • useLayoutEffect里的任务最后影响了Layout

为了用户体验,优先使用useEffect(优先渲染)

useMemo

特点

  • 第一个参数是()=>value
  • 第二个参数是依赖[m,n]
  • 只有依赖变化时,才会计算出新的value
  • 如果依赖不变,那么就重用之前的value
  • 类似于Vue2 的computed,使得一些函数可被重用。

注意

  • 如果value是个函数,那么就要写成useMemo(()=> (x) => console.log(x))
  • 这是一个返回函数的函数
  • 有点难用,于是React还提供了一个useCallback

useCallback

用法

  • useCallback(x => log(x),[m]) 等价于 useMemo( () => x => console.log(x), [m])

useRef

目的

  • 如果需要一个值,在组件不断render时保持不变
  • 初始化:const count = useRef(0)
  • 读取:count.current
  • 为什么需要current?为了保证两次useRef是同一个值(只有引用能做到)

Vue3也有ref,初始化:const count = ref(0); 读取:count.value;不同点:当count.value 变化时,Vue3会自动render

  • useRef能做到变化时自动render吗?不能

  • 因为React的理念是 UI=f(data),如果需要资格功能完全可以自己加,监听ref,当ref.current变化时,调用setX即可

forwardRef

  • 基本用法:让函数组件Button2支持ref

props无法传递ref属性

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

import "./styles.css";

function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按钮</Button2>
      {/* 看浏览器控制台的报错 */}
    </div>
  );
}

const Button2 = props => {
  return <button className="red" {...props} />;
};

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

实现ref传递

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

import "./styles.css";

function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按钮</Button3>
    </div>
  );
}

const Button3 = React.forwardRef((props, ref) => {
  return <button className="red" ref={ref} {...props} />;
});

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

两次传递得到button的引用

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

import "./styles.css";

function App() {
  const MovableButton = movable(Button2);
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.curent);
  });
  return (
    <div className="App">
      <MovableButton name="email" ref={buttonRef}>
        按钮
      </MovableButton>
    </div>
  );
}

// function Button2(props) {
//   return <button {...props} />;
// }

const Button2 = React.forwardRef((props, ref) => {
  return <button ref={ref} {...props} />;
});

// 仅用于实验目的,不要在公司代码中使用
function movable(Component) {
  function Component2(props, ref) {
    console.log(props, ref);
    const [position, setPosition] = useState([0, 0]);
    const lastPosition = useRef(null);
    const onMouseDown = e => {
      lastPosition.current = [e.clientX, e.clientY];
    };
    const onMouseMove = e => {
      if (lastPosition.current) {
        const x = e.clientX - lastPosition.current[0];
        const y = e.clientY - lastPosition.current[1];
        setPosition([position[0] + x, position[1] + y]);
        lastPosition.current = [e.clientX, e.clientY];
      }
    };
    const onMouseUp = () => {
      lastPosition.current = null;
    };
    return (
      <div
        className="movable"
        onMouseDown={onMouseDown}
        onMouseMove={onMouseMove}
        onMouseUp={onMouseUp}
        style={{ left: position && position[0], top: position && position[1] }}
      >
        <Component {...props} ref={ref} />
      </div>
    );
  }
  return React.forwardRef(Component2);
}

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

useRef

  • 可以用来引用DOM对象,也可以用来引用普通对象

forwardRef

  • 由于props不包含ref,所以需要forwardRef
  • 为什么props不包含ref?因为大部分时候不需要

useImperativeHandle

不使用useImperativeHandle 的代码

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

import "./styles.css";

function App() {
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.current);
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按钮</Button2>
      <button
        className="close"
        onClick={() => {
          console.log(buttonRef);
          buttonRef.current.remove();
        }}
      >
        x
      </button>
    </div>
  );
}

const Button2 = React.forwardRef((props, ref) => {
  return <button ref={ref} {...props} />;
});

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

用了useImperativeHandle的代码

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

import "./styles.css";

function App() {
  const buttonRef = useRef(null);
  useEffect(() => {
    console.log(buttonRef.current);
  });
  return (
    <div className="App">
      <Button2 ref={buttonRef}>按钮</Button2>
      <button
        className="close"
        onClick={() => {
          console.log(buttonRef);
          buttonRef.current.x();
        }}
      >
        x
      </button>
    </div>
  );
}

const Button2 = React.forwardRef((props, ref) => {
  const realButton = createRef(null);
  const setRef = useImperativeHandle;
  setRef(ref, () => {
    return {
      x: () => {
        realButton.current.remove();
      },
      realButton: realButton
    };
  });
  return <button ref={realButton} {...props} />;
});

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

分析

  • 用于自定义ref的属性

自定义Hook

封装数据操作

示例1

// useList.js
import { useState, useEffect } from "react";

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list,
    setList: setList
  };
};
export default useList;

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: 1, name: "Frank" },
        { id: 2, name: "Jack" },
        { id: 3, name: "Alice" },
        { id: 4, name: "Bob" }
      ]);
    }, 2000);
  });
}

// index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";

function App() {
  const { list, setList } = useList();
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map(item => (
            <li key={item.id}>{item.name}</li>
          ))}
        </ol>
      ) : (
        "加载中..."
      )}
    </div>
  );
}

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

示例2

// useList.js
import { useState, useEffect } from "react";

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list,
    addItem: name => {
      setList([...list, { id: Math.random(), name: name }]);
    },
    deleteIndex: index => {
      setList(list.slice(0, index).concat(list.slice(index + 1)));
    }
  };
};
export default useList;

function ajax() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve([
        { id: "1", name: "Frank" },
        { id: "2", name: "Jack" },
        { id: "3", name: "Alice" },
        { id: "4", name: "Bob" }
      ]);
    }, 2000);
  });
}


// index.js
import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";

function App() {
  const { list, deleteIndex } = useList();
  return (
    <div className="App">
      <h1>List</h1>
      {list ? (
        <ol>
          {list.map((item, index) => (
            <li key={item.id}>
              {item.name}
              <button
                onClick={() => {
                  deleteIndex(index);
                }}
              >
                x
              </button>
            </li>
          ))}
        </ol>
      ) : (
        "加载中..."
      )}
    </div>
  );
}

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

分析

  • 还可以在自定义Hook里使用Context
  • useState只说了不能在if里,没说不能在函数里运行,只有这个函数在函数组件里运行即可

Stale Closure

过时闭包:函数引用的对象是之前产生的那个对象

解决方法:加个依赖让它自动刷新,还需要清除旧的

参考链接

function createIncrement(incBy) {
  let value = 0;
  function increment() {
    value += incBy;
    console.log(value);
  }
  function log() {
    const message = `Current value is ${value}`;
    console.log(message);
  }
  
  return [increment, log];
}
const [increment, log] = createIncrement(1);
increment(); // logs 1
increment(); // logs 2
increment(); // logs 3
// Works!
log();       // logs "Current value is 3"