React Hooks 笔记

198 阅读8分钟

React Hooks

1650000721701.png

什么是hooks?

Hook 的含义

Hook 这个单词的意思是"钩子"。

React Hooks 的意思是,组件尽量写成纯函数,如果需要外部功能和副作用,就用钩子把外部代码"钩"进来。 React Hooks 就是那些钩子。

你需要什么功能,就使用什么钩子。React 默认提供了一些常用钩子,你也可以封装自己的钩子。

所有的钩子都是为函数引入外部功能,所以 React 约定,钩子一律使用use前缀命名,便于识别。你要使用 xxx 功能,钩子就命名为 usexxx。

useState(状态)

使用状态

const [n,setN] = React.useState(0)
const [user,setUser] = React.useState({name:"xiaohong"})

注意事项1;(不可局部更新)

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

function App() {
  const [user,setUser] = useState({name:'Frank', age: 18})
  const onClick = ()=>{
    setUser({
      name: 'Jack'
    })
  }
  return (
    <div className="App">
      <h1>{user.name}</h1>
      <h2>{user.age}</h2>
      <button onClick={onClick}>Click</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

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

注意事项2 :地址要变

useState(obj),如果obj的地址不变,React就会认为没有变化

useState函数

const [state,setstate] =useState(()=>{return initialState
})
//该函数返回初始的state,并且执行一次

setState 接收函数

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( 是用于提高应用性能的,当更新逻辑比较复杂时,我们应该考虑使用useReducer )

useReducer 和 redux 的区别

  • useReducer 是 useState的代替方案,用于 state 复杂变化
  • useReducer 是单个组件状态管理,组件通讯还需要 props
  • redux 是全局状态管理,多组件共享数据

初步使用(实现加一功能)

  • 创建初始化initialState的值
const initialState ={n:0}
  • 创建所有操作reducer(state,action)
const reducer = (state,action) => {
    if(action.type === 'add') {
        return {n:state.n+1}
    } else if(action.type==='mult') {
        return {n:state.n*2}
    } else {
        throw new Error('error')
    }
}
  • 传给useReducer,得到读和写的api
const App = (PROPS)=>{
    //得到读和写的api
    const [state,dispatch] = React.useReducer(reducer,initialState)
    const {n} = state
    return(
        <div>
           <div>
               <p>{n}</p>
               <button onClick=
                   //使用写的操作实现加一功能
                   {()=>dispatch({type:'add'})}>+1</button>
           </div>
        </div>
    )
}
export default App

useReducer和useState的复杂版

import React, { useState, useReducer } from "react";

const initial = {
    n: 0
};
const reducer = (state, action) => {
    if (action.type === "add") {
        return { n: state.n + action.number };
    } else if (action.type === "multi") {
        return { n: state.n * 2 };
    } else {
        throw new Error("unknown type");
    }
};
function App() {
    const [state, dispatch] = useReducer(reducer, initial);
    const { n } = state;
    const onClick = () => {
        dispatch({ type: "add", number: 1 });
    };
    const onClick2 = () => {
        dispatch({ type: "add", number: 2 });
    };
    return (
        <div className="App">
            <h1>n: {n}</h1>

            <button onClick={onClick}>+1</button>
            <button onClick={onClick2}>+2</button>
        </div>
    );
}
export default App

useReducer 的表单例子

import React, { useState, useReducer } from "react";
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();
    }
}

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>
    );
}
export default App

如何替代Redux(步骤)

  • 将数据集中在一个store对象
  • 将所有操作集中在reducer
  • 创建一个Context
  • 创建对数据的读写api
  • 将第四步内容放到第三步的Context
  • 用Context.Provider将Context提供给所有组件
  • 各个组件用useContext获取读写api

示例代码

import React, { useReducer, useContext, useEffect } from "react";

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>
    );
}


// 帮助函数

// 假 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);
    });
}

export default App

如何做代码优化? 查看代码

useContext(上下文)

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

使用方法

  • 使用C=createConText(initial)创建上下文
  • 使用<C.provider>圈定作用域
  • 在作用域内使用useContext(C)来使用上下文

示例代码

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

import "./styles.css";

const C = createContext(null);

function App() {
  console.log("App 执行了");
  const [n, setN] = useState(0);
  return (
    <C.Provider value={{ n, setN }}>
      <div className="App">
        <Baba />
      </div>
    </C.Provider>
  );
}

function Baba() {
  const { n, setN } = useContext(C);
  return (
    <div>
      我是爸爸 n: {n} <Child />
    </div>
  );
}

function Child() {
  const { n, setN } = useContext(C);
  const onClick = () => {
    setN(i => i + 1);
  };
  return (
    <div>
      我是儿子 我得到的 n: {n}
      <button onClick={onClick}>+1</button>
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

注意 :不是响应式的,你在一个模块将C里面的值改变,零一个模块不会感知到这个变化

useEffect(副作用)

对环境的改变即为副作用,如修改document.title

用途模拟生命周期

  • 模拟(componentDidMount)使用,[]作第二个才是
    useEffect(()=>{
        console.log('第一次渲染')
    },[])
  • 模拟(componentDidUpdate)使用,可指定依赖
    useEffect(()=>{
        console.log('n变啦')
    },[n])
  • 模拟(componentWillUnmount)使用 ,通过return 函数
    useEffect(()=>{
        return ()=>{
            console.log('我要消失啦')
        }
    })

特点: 如果同时存在多个useEffect,会按照出现次序执行

useLayoutEffect(布局副作用)

useLayoutEffect在浏览器渲染完成后执行

import React, { useLayoutEffect,useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const BlinkyRender = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value]);
   useLayoutEffect(()=>{
        console.log('useLayoutEffect')
    },[value])

  return (
    <div id="x" onClick={() => setValue(0)}>value: {value}</div>
  );
};

ReactDOM.render(
  <BlinkyRender />,
  document.querySelector("#root")
);

useLayoutEffect 在浏览器渲染前执行

通过时间点来侧面证明

特点

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

为了用户体验,优先使用useEffect

useMemo

React.memo(在组件变化是才渲染)

示例代码

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

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m}/>
      {/* <Child2 data={m}/> */}
    </div>
  );
}
const child = (props)=> {
  console.log("child 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}
const Child2 = React.memo(child);
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

代码中的Child用React.memo(chlid代替

如果Props不变,就没有必要再次执行函数组件

作用效果代码代码

代码(bug)

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

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClickChild = () => {
    console.log(m);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
      {/* Child2 居然又执行了 */}
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={props.onClick}>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

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

上面的代码运行点击按钮之后,Child2执行了?

因为App运行时会在执行第12行,生成新的函数。新旧函数虽然功能一样,但是地址不一样!所有Child2执行了

用useMemo可以解决

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

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClick2 = () => {
    setM(m + 1);
  };
  const onClickChild = useMemo(() => {
    const fn = div => {
      console.log("on click child, m: " + m);
      console.log(div);
    };
    return fn;
  }, [m]); // 这里呃 [m] 改成 [n] 就会打印出旧的 m
  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
        <button onClick={onClick2}>update m {m}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={e => props.onClick(e.target)}>child: {props.data}</div>;
}

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

useMemo (的特点)

  • 第一个参数树()=>value
  • 第二个参数是依赖[m,n],只能当依赖变化时,才会计算出新的value。如果依赖不变,那么就重用之前的value

注意

  • 如果你的value是个函数,那么你就要写成
useMemo(()=>(X)=>console.log(x))

这是一个返回函数的函数。很难用,于是就有了useCsllbsck

useCallback(语法糖)

用法

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

useRef(不变的值)

目的

如果你需要一个值,在组件不断render是保持不变,初始化值

const count = useRef(0)

读取值

count.current

为什么需要current?

为了保证两次useRef是同一个值(只有引用能做到)

看看vue3 的ref

初始化:const count = ref(0)
读取:count.value

不同点:单count.value变化时,vue3 会自动render

useRef不能做到自动render

forwardRef

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);

两次ref传递得到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

由于propos不包含ref,所以需要forwardRef

为什么props不包含ref呢?因为大部分时候不需要

useImperativeHandle(用于自定义ref属性)

应该叫setRef

不用 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);

自定义Hooks

  • 示例1
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
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);