React Hook - Hook规则

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了React Hook - Hook规则相关的知识,希望对你有一定的参考价值。

参考技术A

Hook 本质其实就是函数,但是我们在使用他们的时候需要遵守两条规则:

eslint-plugin-react-hooks 这个 ESLint 插件来强制执行这两条规则。

我们可以在单个组件中使用多个 State Hook 或 Effect Hook

那么 React 怎么知道哪个 state 对应哪个 useState?答案是 React 靠的是 Hook 调用的顺序。

因为我们的示例中,Hook 的调用顺序在每次渲染中都是相同的,所以它能够正常工作:

只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。但如果我们将一个 Hook (例如 persistForm effect) 调用放到一个条件语句中会发生什么呢?

在第一次渲染中 name !== \'\' 这个条件值为 true,所以我们会执行这个 Hook。但是下一次渲染时我们可能清空了表单,表达式值变为 false。此时的渲染会跳过该 Hook,Hook 的调用顺序发生了改变:

React 不知道第二个 useState 的 Hook 应该返回什么。React 会以为在该组件中第二个 Hook 的调用像上次的渲染一样,对应的是 persistForm 的 effect,但并非如此。从这里开始,后面的 Hook 调用都被提前执行,导致 bug 的产生。

这就是为什么 Hook 需要在我们组件的最顶层调用。 如果我们想要有条件地执行一个 effect,可以将判断放到 Hook 的内部:

原因是:React 靠的是 Hook 调用的顺序,来寻找每次组件调用时变量的对应关系。

React Hook要点笔记

文章目录


hook 是有状态的函数.

使用规则

  • 只在最顶层使用 Hook

不要在循环,条件或嵌套函数中调用 Hook

  • 只在 React 函数中调用 Hook

不要在普通的 JavaScript 函数中调用 Hook

state hook

和class component相比,state hook采用的是替换而非合并,因此可将state逐一声明.

useReducer和useContext

对于复杂的state操作逻辑,嵌套的state的对象,推荐使用useReducer。

context

Context的作用就是对它所包含的组件树提供全局共享数据的一种技术

useContext以Hook的方式使用React Context。

接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值。当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定。

// 第一步:创建需要共享的context
const ThemeContext = React.createContext('light');

class App extends React.Component 
  render() 
    // 第二步:使用 Provider 提供 ThemeContext 的值,Provider所包含的子树都可以直接访问ThemeContext的值
    return (
      <ThemeContext.Provider value="dark">
        <Toolbar />
      </ThemeContext.Provider>
    );
  

// Toolbar 组件并不需要透传 ThemeContext
function Toolbar(props) 
  return (
    <div>
      <ThemedButton />
    </div>
  );


function ThemedButton(props) 
  // 第三步:使用共享 Context
  const theme = useContext(ThemeContext); 
  render() 
    return <Button theme=theme />;
  

子孙类组件出发reducer状态变化。就是将dispatch函数作为context的value,共享给页面的子组件。

// 定义初始化值
const initState = 
    name: '',
    pwd: '',
    isLoading: false,
    error: '',
    isLoggedIn: false,

// 定义state[业务]处理逻辑 reducer函数
function loginReducer(state, action) 
    switch(action.type) 
        case 'login':
            return 
                ...state,
                isLoading: true,
                error: '',
            
        case 'success':
            return 
                ...state,
                isLoggedIn: true,
                isLoading: false,
            
        case 'error':
            return 
                ...state,
                error: action.payload.error,
                name: '',
                pwd: '',
                isLoading: false,
            
        default: 
            return state;
    

// 定义 context函数
const LoginContext = React.createContext();
function LoginPage() 
    const [state, dispatch] = useReducer(loginReducer, initState);
    const  name, pwd, isLoading, error, isLoggedIn  = state;
    const login = (event) => 
        event.preventDefault();
        dispatch( type: 'login' );
        login( name, pwd )
            .then(() => 
                dispatch( type: 'success' );
            )
            .catch((error) => 
                dispatch(
                    type: 'error'
                    payload:  error: error.message 
                );
            );
    
    // 利用 context 共享dispatch
    return ( 
        <LoginContext.Provider value=dispatch>
            <...>
            <LoginButton />
        </LoginContext.Provider>
    )

function LoginButton() 
    // 子组件中直接通过context拿到dispatch,出发reducer操作state
    const dispatch = useContext(LoginContext);
    const click = () => 
        if (error) 
            // 子组件可以直接 dispatch action
            dispatch(
                type: 'error'
                payload:  error: error.message 
            );
        
    

参考

effect hook

可以把 useEffect Hook看做componentDidMountcomponentDidUpdatecomponentWillUnmount 这三个函数的组合。

通常我们都会在第一次dom渲染完成以及后续dom重新更新时,去调用我们的副作用操作。

Effect在默认情况下,会在第一次渲染之后和每次更新之后都会执行,这也就让我们不需要再去考虑是componentDidMount还是componentDidUpdate时执行,只需要明白Effect在组件渲染后执行即可.

可以使用Effect来清除这些副作用,只需要在Effect中返回一个函数即可:

seEffect(() => 
    pollingNewStatus()
    //告诉React在每次渲染之前都先执行cleanup()
    return function cleanup() 
      unPollingNewStatus()
    ;
 );

有个明显的区别在于useEffect其实是每次渲染之前都会去执行cleanup(),而componentWillUnmount只会执行一次。

在Effect中,我们可以通过增加Effect的第二个参数即可,如果没有变化,则跳过更新:

useEffect(() => 
  document.title = `You clicked $count times`;
, [count]); // 仅在 count 更改时更新

useCallback

把函数式组件理解为class组件render函数的语法糖,所以每次重新渲染的时候,函数式组件内部所有的代码都会重新执行一遍。

通过useCallback获得一个记忆后的函数。第二个参数传入一个数组,数组中的每一项一旦值或者引用发生改变,useCallback就会重新返回一个新的记忆函数提供给后面进行渲染。如果依赖项没有变,那么记忆函数的引用就不会变,当函数作为props传递给子组件时,就不会引发子组件的重新渲染:

function App() 
  const memoizedHandleClick = useCallback(() => 
    console.log('Click happened')
  , []); // 空数组代表无论什么情况下该函数都不会发生改变
  return <SomeComponent onClick=memoizedHandleClick>Click Me</SomeComponent>;

使用场景是:有一个父组件,其中包含子组件,子组件接收一个函数作为props;通常而言,如果父组件更新了,子组件也会执行更新;但是大多数场景下,更新是没有必要的,我们可以借助useCallback来返回函数,然后把这个函数作为props传递给子组件;这样,子组件就能避免不必要的更新。

所有依赖本地状态或props来创建函数,需要使用到缓存函数的地方,都是useCallback的应用场景。

参考: useMemo与useCallback使用指南

useMemo

useCallback 的功能完全可以由useMemo所取代.

useCallback(fn, inputs) is equivalent to useMemo(() => fn, inputs).

唯一的区别是:useCallback不会执行第一个参数函数,而是将它返回给你,而useMemo会执行第一个函数并且将函数执行结果返回给你。

当useCallback依赖项没有改变时,返回缓存的函数,否则返回一个新的函数。

而useMemo更适合经过函数计算得到一个确定的值,比如记忆组件或者计算的值,在依赖项没变时返回缓存的值。

function Parent( a, b ) 
  // Only re-rendered if `a` changes:
  const child1 = useMemo(() => <Child1 a=a />, [a]);
  // Only re-rendered if `b` changes:
  const child2 = useMemo(() => <Child2 b=b />, [b]);
  return (
    <>
      child1
      child2
    </>
  )

只有依赖项a/b发生改变时,才会触发相应子组件的重新渲染。

useRef 保存引用值

可以保存对dom/组件的引用,此外可以很方便地保存任何可变值,其类似于在class中使用实例属性。在组件生命周期的每次渲染时返回同一个ref对象,而不像state返回一个新的。可以用来跨越渲染周期存储数据(即多次渲染仍然是返回同一个引用对象)。

常见的用来存储定时器。

写入它会被视为“副作用”,因此咱们无法在渲染过程中更改它,需要在useEffect hook 中才能修改。

变更 .current 属性不会引发组件重新渲染。

useLayoutEffect

useEffect传入的回调是异步的。useLayoutEffect中的副作用会在DOM更新之后立马同步执行,和原来componentDidMount&componentDidUpdate一致,在react完成DOM更新后马上同步调用的代码,会阻塞页面渲染。

自定义hook

通过自定义hook来封装组件要共享的业务逻辑。自定义hook就是以use开头且调用其他hook的函数。

自定义的hook强大之处在于拥有状态,当状态值改变时,它可以触发调用组件的重新渲染,而且自定义hook可以跟随着组件的生命周期,在不同的生命钩子阶段,我们可以处理一些事件。

UI组件只需要去消费hook产出的value和function。状态处理都封装在了自定义hook里。只要符合它们的接口格式约定,UI组件就可以随时随地地复用这些逻辑。

自定义hook的设计范式:

const  state, handleChange, others  = useCustomHook(config, dependency?);
  • config声明了hook所需要的数据,可能是内部useState的初始值,也可能是结构化的数据,总结起来就是这个hook的配置
  • dependency通常只有hook内使用了useEffect、useCallback这类API,需要我们声明依赖的时候需要传入。

参考:

以上是关于React Hook - Hook规则的主要内容,如果未能解决你的问题,请参考以下文章

React Hook要点笔记

React进阶:HOOK

react hook 新特性汇总

关于React hook,我做了个违背祖训的决定

react讲解(函数式组件,react hook)

react讲解(函数式组件,react hook)