跳到主要内容

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