跳到主要内容

React Hooks

本文所有栗子均在: https://codesandbox.io/s/hook-e49wk

函数组件 App,在每一次渲染都会被调用,而每一次调用都会形成一个独立的上下文,可以理解成一个快照。每一次渲染形成的快照,都是互相独立的。

实时编辑器
//函数组件每一次渲染的独立上下文
function App() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setTimeout(() => {
      console.log('点击次数: ' + count);
    }, 3000);
  };
  //1、点击增加按钮两次,将 count 增加到 2。
  //2、点击「展示点击次数」。
  //3、在 console.log 执行之前,也就是 3 秒内,再次点击新增按钮 2 次,将 count 增加到 4。
  //打印出2,而不是4。
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>点击{count}</button>
      <button onClick={handleClick}>展示点击次数</button>
    </div>
  );
}
结果
Loading...

使用 Hooks 的规则:

  1. 总在组件的顶部调用 Hooks,不能在循环,条件或嵌套函数中使用 Hooks。
  2. 只能在函数组件中使用 Hooks 或者在自定义 Hooks 中调用 Hooks。不要在普通的 js 函数中调用 Hooks。

useEffect

解决的问题:EffectHook 用于函数式组件中副作用,执行一些相关的操作,逻辑聚合。

所谓副作用,不在渲染过程中产生的作用。

useEffect 的执行

依赖 deps:每次 deps 改变就会执行回调函数(useEffect 的第一个参数)。如果不传 deps,只要该组件有 state 改变就会触发回调函数。如果 deps 为一个空数组,回调函数只会在该组件初始化时执行一次。

依赖项如果是对象,只能浅比较,是不是同一个对象(通过Object.is的方法比较)。如果需要深比较,可以使用 useDeepCompareEffect

在 useEffect 的第一个参数中 return 一个清除函数,这个函数将在组件卸载的时候执行,因此在这里可以移除监听等在卸载时执行的操作。

每次渲染函数组件时,useEffect 都是新的,都是不一样的。deps 为一个空数组时,callback 只会在组件初始化时执行一次,清除函数在组件卸载时执行。deps 不为空,每次 deps 变化时,都会先执行清除函数,然后执行 callback。

实时编辑器
// useEffect 每次重新渲染都是新的
function App() {
  const [count, setCount] = useState(0);
  useEffect(() => {
    setTimeout(() => {
      console.log('点击次数: ' + count);
    }, 3000);
  }); //没有deps,组件重新渲染时,会重新执行 useEffect 内的回调,并且里面 count 值也是当时的快照的一个常量值。
  return (
    <div className="App">
      <button onClick={() => setCount(count + 1)}>点击{count}</button>
    </div>
  );
}
结果
Loading...
提示

关于依赖

下面的 useCallback,useMemo 的第二个参数同 useEffect 一致,用于监听变量,如在数组内添加 name、phone 等参数,当改变其中的值,都会触发子组件副作用的执行。

如果不添加依赖,则在任何重新渲染时都会执行。

useMemo

用于缓存一个值。

const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);

可以用于解决父组件更新引起子组件更新的问题,告诉子组件需要在什么时候更新,什么时候不更新。相当于把父组件需要传递的参数做了一个标记,参数更新时更新子组件。无论父组件其他状态更新任何值,都不会影响要传递给子组件的对象。

实时编辑器
function Child({ data }) {
  useEffect(() => {
    console.log('查询条件:', data);
  }, [data]);

  return <div>子组件</div>;
}

function App() {
  const [name, setName] = useState('');
  const [phone, setPhone] = useState('');
  const [kw, setKw] = useState('');

  // const data = {
  //   name,
  //   phone
  // };
  //如果按照上面的部分,即使child的props 只有name,phone,没有kw,但修改kw,父组件重新渲染也会导致子组件重新渲染。
  //下面把导致重新渲染的值用useMemo包起来,使kw变化,父组件改变不影响子组件。
  const data = useMemo(
    () => ({
      name,
      phone,
    }),
    [name, phone],
  );

  return (
    <div className="App">
      <input
        onChange={(e) => setName(e.target.value)}
        type="text"
        placeholder="请输入姓名"
      />
      <input
        onChange={(e) => setPhone(e.target.value)}
        type="text"
        placeholder="请输入电话"
      />
      <input
        onChange={(e) => setKw(e.target.value)}
        type="text"
        placeholder="请输入关键词"
      />
      <Child data={data} />
    </div>
  );
}
render(<App />);
结果
Loading...
警告

传递给 useMemo 的函数在渲染期间运行,注意里面的逻辑不要再次触发渲染,副作用应该放在 useEffect 里面。

如果不提供依赖数组,则会在每次渲染时都重新计算。

将 useMemo 作为性能优化,而不是语义保证,因为 React 有可能在某些情况下忘掉记住的值,重新计算。

React.memo 与 useMemo

长得比较像,开始总是弄混。

React.memo 是包装整个组件,只是浅比较 props 来确定是否重新渲染,当然可以手动写第二个参数比较具体 props 的不同来进行 re-render。对组件外层进行包装,控制整个组件是否重新渲染。

useMemo 是实现局部 pure 的功能,控制组件的部分内容不要 re-render,而不是整个组件是否重新渲染。

实时编辑器
const Child = (props = {}) => {
  console.log(`--- re-render ---`, props);
  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
};
const isEqual = (prevProps, nextProps) => {
  if (prevProps.number !== nextProps.number) {
    return false;
  }
  return true;
};
const ChildMemo = memo((props = {}) => {
  console.log(`--- memo re-render ---`, props);
  return (
    <div>
      <p>number is : {props.number}</p>
    </div>
  );
}, isEqual);
const App = (props = {}) => {
  const [step, setStep] = useState(0);
  const [count, setCount] = useState(0);
  const [number, setNumber] = useState(0);

  const handleSetStep = () => {
    setStep(step + 1);
  };

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

  const handleCalNumber = () => {
    setNumber(count + step);
  };

  return (
    <div>
      <button onClick={handleSetStep}>step is : {step} </button>
      <button onClick={handleSetCount}>count is : {count} </button>
      <button onClick={handleCalNumber}>numberis : {number} </button>
      <hr />
      <Child step={step} count={count} number={number} /> <hr />
      <ChildMemo step={step} count={count} number={number} />
    </div>
  );
};
render(<App />);
结果
Loading...
//当然也可以用 useMemo 来缓存一个函数组件的返回值,也可以减少组件的重新渲染。
const ChildUseMemo = (props = {}) => {
console.log(`--- component re-render ---`);
//useMemo 包裹子组件渲染部分的逻辑。父组件更新时,子组件会重新执行,但并不会重新渲染
return useMemo(() => {
console.log(`--- useMemo re-render ---`);
return (
<div>
<p>number is : {props.number}</p>
</div>
);
}, [props.number]);
};

useCallback

实时编辑器
function Child({ callback }) {
  useEffect(() => {
    callback();
  }, [callback]);

  return <div>子组件</div>;
}
function App() {
  const [name, setName] = useState('');
  const [phone, setPhone] = useState('');
  const [kw, setKw] = useState('');
  // const callback = () => {
  //   console.log('我是callback')
  // }
  //按照上面,父组件的重新渲染就会导致子组件重新渲染,给子组件添加依赖什么重新渲染,作为性能优化。
  const callback = useCallback(() => {
    console.log('我是callback');
  }, []);
  return (
    <div className="App">
      <input
        onChange={(e) => setName(e.target.value)}
        type="text"
        placeholder="请输入姓名"
      />
      <input
        onChange={(e) => setPhone(e.target.value)}
        type="text"
        placeholder="请输入电话"
      />
      <input
        onChange={(e) => setKw(e.target.value)}
        type="text"
        placeholder="请输入关键词"
      />
      <Child callback={callback} />
    </div>
  );
}

render(<App />);
结果
Loading...

useMemo 和 useCallback,都能为「重复渲染」这个问题,提供很好的帮助。useCallback 是「useMemo 的返回值为函数时的特殊情况,useCallback(fn, deps) 相当于 useMemo(() => fn, deps)

useCallback 配合 React.memo 减少不必要的渲染

实时编辑器
//使用 React.memo 将子组件作为 pureComponent,减少不必要的渲染。useCallback 缓存 props 中的函数,减少 props 不必要的变化导致的渲染。
const Child = React.memo(function ({ val, onChange }) {
  console.log('render...', val);
  return <input value={val} onChange={onChange} />;
});

function App() {
  const [val1, setVal1] = useState('');
  const [val2, setVal2] = useState('');

  //如果不用useCallback, 任何一个输入框的变化都会导致另一个输入框重新渲染.
  //一个输入框变化,父组件重新渲染,导致生成新的onChange函数,props 变化了,则子组件也重新渲染
  const onChange1 = useCallback((evt) => {
    setVal1(evt.target.value);
  }, []);

  const onChange2 = useCallback((evt) => {
    setVal2(evt.target.value);
  }, []);

  return (
    <>
      <Child val={val1} onChange={onChange1} />
      <Child val={val2} onChange={onChange2} />
    </>
  );
}
render(<App />);
结果
Loading...

useCallback 配合使用 useEffect 实现按需加载

useCallback 支持我们缓存某一函数,当且仅当依赖项发生变化时,才更新该函数。这使得我们可以在子组件中配合 useEffect ,实现按需加载。

实时编辑器
function Parent() {
  const [count, setCount] = useState(0);
  const [query, setQuery] = useState('keyword');

  const getData = useCallback(() => {
    const url = `https://mocks.alibaba-inc.com/mock/fO87jdfKqX/demo/queryData.json?query=${query}`;
    // 请求数据...
    console.log(`请求路径为:${url}`);
  }, [query]); // 当且仅当 query 改变时 getData 才更新

  // 计数值的变化并不会引起 Child 重新请求数据
  return (
    <>
      <h4>计数值为:{count}</h4>
      <button onClick={() => setCount(count + 1)}> +1 </button>
      <input
        onChange={(e) => {
          setQuery(e.target.value);
        }}
      />
      <Child getData={getData} />
    </>
  );
}

function Child({ getData }) {
  useEffect(() => {
    getData();
  }, [getData]); // 函数可以作为依赖项参与到数据流中

  return <p>child</p>;
}
render(<Parent />);
结果
Loading...

了解更多: 你不知道的 useCallback - SegmentFault 思否

useContext

Context 是在组件树中自上而下地跨组件传递数据,不必显式地通过组件树逐级传递 props。

应用于在很多不同层级的组件间访问同样一些数据,但是会使组件复用性变差。有时可以用组件组合代替。

具体使用场景:管理当前的 locale,theme,userInfo 或者一些缓存数据,比替代方案要简单的多。

替代方案:redux 等状态管理工具、webStorage、props 层层传递等。

提示

Provider 的 value 值发生变化时,它内部的所有消费组件都会重新渲染。

如果被 Provide 包裹的组件内部没有使用 value 里的值,可以将组件用 React.memo 包裹或者使用 shouldComponentUpdate,或者使用 useMemo 将组件缓存起来(利用 React 本身对 React element 对象的缓存)减少渲染。

但是如果组件使用了 value,在 value 值发生变化时都会重新渲染。

实时编辑器
const UserContext = React.createContext('default');
const ChannelContext = React.createContext('channel');
//只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。

//两种消费方式
//①
function ComponentC() {
  return (
    <UserContext.Consumer>
      {/** 接收当前 context 值,返回一个React 节点 **/}
      {/** 当使用多个 context 时,这种消费方式结构会比较复杂 **/}
      {(user) => <div>CCCCCC User context value {user}</div>}
    </UserContext.Consumer>
  );
}

//②
function ComponentE() {
  //使用多个context 的时候,useContext 相比consumer 更优雅简洁
  const user = useContext(UserContext);
  const channel = useContext(ChannelContext);
  console.log('user Render');
  return (
    <div>
      FFFFFFF {user} - {channel}
    </div>
  );
}
const ComponentF = React.memo(ComponentE);

const App = () => {
  const [user, setUser] = useState('');
  const changeUser = (e) => {
    setUser(e.target.value);
  };
  return (
    <div className="App">
      <input value={user} onChange={changeUser} />
      <UserContext.Provider value={user}>
        {/* Provider变化会引起内部组件重新渲染 */}
        <ComponentC />
        <ComponentF />
      </UserContext.Provider>
    </div>
  );
};

render(<App />);
结果
Loading...

Context 的特性

  • Provider 作为 context 的提供者,value 更新会导致包裹的所有组件重新更新。

  • 多个不同的/相同的 Provider 之间可以相互嵌套。

  • 同一个 context 可以逐层嵌套多个 Provider,里面的 value 的值可以不同。下一层级的 Provider 可以覆盖上一层及的 Provider。

useReducer

相比于 useState,useReducer 更适合:

state 逻辑处理较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等场景。

每次 state 变化时,都会触发一次重新渲染。

实时编辑器
const initialState = { count: 0 };
function reducer(state, action) {
  //接收当前 state 和 action, 并根据不同的 action 返回不同的新的 state。
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    case 'reset':
      return { count: 0 };
    default:
      return state;
  }
}
function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      {/* dispatch 一个action */}
      <button onClick={() => dispatch({ type: 'decrement' })}>-</button>
      <button onClick={() => dispatch({ type: 'increment' })}>+</button>
      <button onClick={() => dispatch({ type: 'reset' })}>reset</button>
    </>
  );
}
render(<Counter />);
结果
Loading...

useState,useReducer 都提供了惰性初始化的方式。可以通过函数计算初始值。

useReducer + useContext 实现全局 state:hook - CodeSandbox

即在上层组件封装useReducer,将[state,dispatch]通过provider广播出去,在下层任意组件使用。

useReducer 实现 todo:react-todo - CodeSandbox

useReducer 中的 reducer 不支持异步,配合使用异步:hook - CodeSandbox

了解更多:

React Hooks 系列之 4 useReducer - 掘金

reactjs - React useReducer async data fetch - Stack Overflow

React Hooks: useState 和 useReducer 有什么区别? · 语雀

提示

useReducer 与 useState

React 内部的 useState 是通过 useReducer 实现的,setState 内部封装了一个 dispatch。

useState 适合处理结构简单的 State,算是一个在使用上更简单的 useReducer。

useReducer 适合处理简易的组件间数据流管理,比 Redux 更轻量。

UseRef

返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。

可以用 Ref 指向一个 dom 来控制它的变化,另外也可以用来存放变量,比如 setTimeout,setInterval,存起来方便在合适的时机清除。

实时编辑器
//访问DOM元素
function TextInputWithFocusButton() {
  const inputRef = useRef(null);
  const onButtonClick = () => {
    // `current` 指向已挂载到 DOM 上的文本输入元素
    inputRef.current.focus();
  };
  return (
    <>
      <input ref={inputRef} type="text" />
      <button onClick={onButtonClick}>Focus the input</button>
    </>
  );
}
结果
Loading...
提示

useRef 会在每次渲染时返回同一个 ref 对象。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。

forwardRef

ref 转发,方便父组件拿到子组件的实例。把自身外面的 ref 转发到内部的组件,使写在自己身上的 ref 不指向自己。

实时编辑器
function A(props, parentRef) {
  return (
    <div>
      <input type="text" ref={parentRef} />
    </div>
  );
}
let ForwardChild = forwardRef(A); // 把子组件包裹起来
function App() {
  const parentRef = useRef();
  function focusHander() {
    console.log('input的value', parentRef.current.value);
  }
  return (
    <div>
      <ForwardChild ref={parentRef} />
      <button onClick={focusHander}>获取焦点</button>
    </div>
  );
}
render(<App />);
结果
Loading...

useImperativeHandle

一般结合 forwardRef 使用,在 ref 转发到组件内部时,选择暴露一些特定的值或方法给父组件。

为什么使用:

  • useImperativeHandle 可以让你在使用 ref 时,自定义暴露给父组件的实例值,不能让父组件想干嘛就干嘛

  • 通过 useImperativeHandle ,子组件还可以使用很多的 ref,可以暴露给父组件操作子组件内部的多个 ref

const Child = forwardRef<HTMLInputElement, {}>((props, parentRef) => {
const inputRef = useRef<HTMLInputElement>(null);
const [name, setName] = useState('默认name');
// 把子组件A 内部的一些值或方法暴露给父组件使用
useImperativeHandle(parentRef, () => {
return {
name,
};
});
return (
<div>
<input
type="text"
ref={inputRef}
onChange={(e) => setName(e.target.value)}
/>
</div>
);
});

function App() {
//使用时需要使用 ElementRef 泛型,并使用 typeof 获取组件的 ref 类型
const parentRef = useRef<ElementRef<typeof ForwardChild>>(null);
//parentRef.current 拿到的是子组件通过useImperativeHandle 返回的一个对象
const say = () => {
console.log(parentRef.current.name);
};
return (
<>
<Child ref={parentRef} />
<button onClick={say}>打印子组件name</button>
</>
);
}
render(<App />);

自定义 Hook

使用 use 开头,调用一些 hook,封装自己的逻辑。

比如有一个请求公共数据的接口,在多个页面中被重复使用,你便可通过自定义 Hook 的形式,将请求逻辑提取出来公用。

自定义 Hook 在同一个组件内使用多次,hooks 内的 state 和副作用都是完全隔离的,不用担心它们会互相干扰。

实现一些 custom Hooks hook - CodeSandbox

Reference