Hooks

281 阅读5分钟

Hooks是一种复用状态逻辑的方式,并不复用state本身。Hook的每次调用都有一个完全独立的state,所以你可以在单个组件中多次调用同一个自定义hook** Hook是React16.8的新增特性,只能在函数组件中使用,它是一个特殊的函数。

useState

允许你在React函数组件中添加state 的 Hook。一般来说,变量在函数退出后就会消失,而state中的变量会被React保留。

import React, { useState } from 'react';
const App = () => {
 const [count, setCount] = useState(0);
  return (
    <div>
      <div>点击了{ count }次</div>
      <button onClick={() => { setCount(count + 1) }}>+</button>
    </div>
  )
}

函数式更新: 如果新的state需要通过使用先前的state计算得出,那么可以将函数传递给setState

setCount(prevCount => prevCount + 1))

惰性初始state: 如果初始state需要通过复杂计算得到,则可以传入一个函数,在函数中计算并返回初始state, 此函数只在初始渲染时被调用

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

useEffect

useEffect可以让你在函数组件中执行副作用操作

副作用:数据获取、设置订阅、操作DOM都属于副作用

默认情况下,它会在第一次渲染之后和每次更新之后都会执行。React保证每次运行effect的同时,DOM都已经更新完毕。

如果effect返回一个函数,React将会在执行清除操作时调用它。

function FriendStatusWithCounter(props) {
  // React会按照effect声明的顺序依次调用组件中的每一个effect
  const [count, setCount] = useState(0);
  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  const [isOnline, setIsOnline] = useState(null);
  useEffect(() => {
    function handleStatusChange(status) {
      setIsOnline(status.isOnline);
    }

    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });
  // ...
}

React何时清除effect?

React会在组件卸载的时候执行清除操作,effect在每次渲染的时候都会执行。React会在执行当前effect之前对上一个effect进行清除。

useContext

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

const myContext = createContext(null)
const { Provider } = myContext

const { value } = useContext(myContext)

const themes = {
  light: {
    foreground: "#000000",
    background: "#eeeeee"
  },
  dark: {
    foreground: "#ffffff",
    background: "#222222"
  }
};

const ThemeContext = React.createContext(themes.light);

function App() {
  return (
    <ThemeContext.Provider value={themes.dark}>
      <Toolbar />
    </ThemeContext.Provider>
  );
}

function Toolbar(props) {
  return (
    <div>
      <ThemedButton />
    </div>
  );
}

function ThemedButton() {
  const {foreground, background } = useContext(ThemeContext);
  return (
    <button style={{ background: background, color: foreground }}>
      I am styled by theme context!
    </button>
  );
}

useReducer

它是useState的替代方案

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

const [state, dispatch] = useReducer(reducer, initialArg, init)

const reducer = (state, action) => {
  return ...
}
const initialState = {count: 0};

function reducer(state, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1};
    default:
      throw new Error();
  }
}

function Counter() {
  const [state, dispatch] = useReducer(reducer, initialState = { count: 0 });
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
    </>
  );
}

惰性初始化 需要将init函数作为useReducer的第三个参数传入,这样初始state将被设置为init(initialArg)

function init(initialCount) {
 return {
  count: initialCount
 }
}

const [state, dispatch] = useReducer(reducer, initialCount, init = function)

useMemo

会返回一个memoized值,仅在依赖项改变时才会重新计算memoized值。这种优化有助于避免在每次渲染时都进行高开销的计算

注意: 传入useMemo的函数会在渲染期间执行,不要在函数内部执行与渲染无关的操作

useMemo(() => fn, deps) === useCallback(fn, deps)

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

useCallback

会返回一个memoized回调函数, 该回调函数仅在某个依赖项改变时才会更新

使用场景: 父组件给子组件传递的方法使用useCallback包裹

useCallback(fn, deps) === useMemo(() => fn, deps)

// 把内联回调函数及依赖项数组作为参数传入 useCallback 
const memoziedCallback = useCallback(() => {
  doSomething(a,b)
}, [a,b])

useRef

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

const iptRef = useRef(null);
<input type="text" ref={iptRef}/>
  iptRef.current.focus() // 获取input的焦点

useImperativeHandle&forwardRef

父组件中如何获取子组件(函数组件)的实例 不能直接通过ref去获取,useImperativeHandle应当与forwardRef一起使用

import React, { useImperativeHandle, forwardRef } from 'react';
// 子组件 Input组件
forwardRef((props, ref) => {
  const iptRef = useRef(null)
  useImperativeHandle(ref, () => ({
    // 提供给父组件的方法
    focus: () => {
      iptRef.current.focus()
    }
  }))
  return <input ref={iptRef} type="text"/>
})

// 父组件中
let formRef = null;
<Input ref={ref => { formRef = ref }} />
// 父组件中获取子组件中的方法
formRef.focus() // formRef.current.focus()

useLayoutEffect

与useEffect相似,会在所有DOM变更之后同步调用effect, 可以使用它来读取DOM布局并同步触发重渲染

使用场景: 将代码从class迁移到Hooks中,使用useEffect出现问题再尝试使用useLayoutEffect

注意: 尽可能使用标准的useEffect以避免阻塞视觉更新

自定义Hook (useHook)

作用: 将组件逻辑提取到可重用的函数中

使用场景: 当我们想在两个函数之间共享逻辑时,可以将它提取到第三个函数中。而组件和Hook都是函数,所以也同样适用这种方式

注意: 自定义hook是一个函数,名称以use开头,函数内部可以调用其他的hook

// 自定义Hook
function useFriendStatus(friendId) {
  const [isOnline, setIsOnline] = useState(null);
  // 会在第一次渲染以及每次更新的时候调用
  useEffect(() => {
    const handleStatusChange = (status) => {
      setIsOnline(status)
    }
    ChatAPI.subscribeToFriendStatus(friendId, handleStatusChange)
    return ChatAPI.unsubscribeFromFriendStatus(friendId, handleStatusChange)
  })
  return isOnline
}

编写自定义的useReducer(在不引入redux库的情况下) 1、返回state 和 dispatch

// 自定义useReducer 返回值 state 和 dispatch
const useReducer = (reducer, initialState) => {
  const [state, setState] = useState(initialState);
  const dispatch = (action) => {
    const nextState = reducer(state, action);
    setState(nextState);
  }
  return [ state, dispatch ]
}

2、在组件中使用

// 在组件中使用
const [todos, dispatch] = useReducer(todoReducer, []);
const handleAddTodo = (text) => {
  dispatch({
    type: 'add',
    text
  })
}

3、todoReducer

// todoReducer
const todoReducer = (state, action) => {
  switch(action.type) {
    case 'add':
      return [...state, { id: Date.now(), text: action.text, completed: false }]
    case 'del':
      return state.filter(todo => todo.id !== action.id)
    case 'toggle':
      return state.map(todo => {
        if (todo.id === action.id) {
          todo.completed = !todo.completed
        }
        return todo
      })
    default:
      return state
  }
}

4、创建一个获取数据的自定义Hooks

// 获取数据的自定义hooks
const useHackerNewsApi = (initialUrl,initialData) => {
  const [data, setData] = useState(initialData)
  const [url, setUrl] = useState(initialUrl)
  const [isLoading, setIsLoading] = useState(false)
  const [isError, setIsError] = useState(false)
  // effect会在第一次渲染以及每次更新的时候执行
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false)
      setIsLoading(true)
      try {
        const res = await axios.get(url)
        setData(res.data)
      } catch(error) {
        setIsError(true)
      }
      setIsLoading(false)
    }
    fetchData()
  }, [url])
  return [{ data, isLoading, isError }, setUrl]
}

// 使用的时候
const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi('https://hn.algolia.com/api/v1/search?query=redux',
{ hits: [] })

// 更新数据的时候
doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);

5、使用useReducer来获取数据(将每一个状态定义在reducer中来管理一个有效状态对象)

Q:如何避免在组件卸载时候 还在执行setState?

A:在effect中定义一个变量didCancel = false, 执行清除操作的时候 将其置为true

const useDataApi = (initialUrl, initialData) => {
  const [url, setUrl] = useState(initialUrl)
  // reducer中可以拿到最新的状态
  const [state, dispatch] = useReducer(dataFetchReducer, {
    isLoading: false,
    isError: false,
    data: initialData
  })
  useEffect(() => {
    let didCancel = false
    const fetchData = async () => {
      dispatch({ type: 'FETCH_INIT' })
      try {
        const res = await axios.get(url)
        // 避免在组件卸载之后 还在执行setState
        if (!didCancel) {
          dispatch({ type: 'FETCH_SUCCESS', payload: res.data })
        }
      } catch(error) {
        if (!didCancel) {
          dispatch({ type: 'FETCH_FAILURE' })
        }
      }
    }
    fetchData()
    // 清除函数会在组件卸载的时候执行
    return () => {
      didCancel = true
    }
  }, [url])
  return [state, setUrl]
}

// 执行过程 setUrl会传递一个最新的url effect依赖url会重新执行 dispatch分发action 然后将action传递给reducer 在reducer里面拿到最新的状态

Hooks FAQ

1、React Redux从v7.1.0开始支持Hook API 并暴露了useDispatch useSelector等Hook hooks在Redux中的使用

函数组件中如何使用React Router 2、从Class迁移到hook

  • 生命周期如何对应到Hook
constructor(props) {
  super(props);
  state = {
  }
}
const [state, setState] = useState(state => {
  return state + 1
})
  • constructor(props) {
      super(props);
      state = {
      }
    }
    const [state, setState] = useState(state => {
      return state + 1
    })
    
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
  • getSnapshotBeforeUpdate,componentDidCatch 以及 getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会被添加。
  • shouldComponentUpdate:详见 下方 React.memo. 3、useRef类似实例变量

通常在事件处理器和effects中修改refs

4、如何在hooks中实现shouldComponentUpdate

可以使用React.memo包裹一个组件来对它的props进行浅比较

const Button = React.memo((props) => {
  // 组件
})
memo等效于PureComponent 但它只比较props, React.memo不比较state,因为没有单一的state对象可供比较

5、如何避免向下传递回调(important)

在大型应用中,推荐的替代方案是通过context使用useReducer 往下传递一个dispatch函数

App组件内的任何子节点都可以使用dispatch函数来向上传递actions到App中

const MyContext = createContext(null)
// 父组件中
const App = () => {
  const [todos, dispatch] = useReducer(todoReducer);
  <MyContext.Provider value={dispatch}>
    <DeepTree todos={todos}></DeepTree>
    <Demo1 todos={todos}></Demo1>
 		<Demo2 todos={todos}></Demo2>
  </MyContext.Provider>
}

App组件内的任何子节点都可以使用dispatch函数来向上传递actions到App中(组件会重新渲染)

// 子组件中
const DeepTree = ({ todos }) => {
  const dispatch = useContext(MyContext)
  const handleClick = () => {
    dispatch({ type: 'add', text: 'hello' })
  }
  return (
    <button onClick={handleClick}>add</button>
  )
}

6、effect的依赖列表

  • 在依赖列表中省略函数不安全。要记住effect外部的函数使用了哪些props和state很难,我们通常会在effect的内部去声明它所需要的函数。这样就能容易的看出effect依赖了组件作用域中的哪些值
  • 只有当函数(以及它所调用的函数)不引用props、state以及由他们衍生而来的值时,才能放心的把它们从依赖列表中删除

  • 什么情况下,依赖项可以是函数

    万不得已的情况下,可以把函数加入effect的依赖但把它的定义包裹进useCallback Hook,这就确保了它不随渲染而改变,除非它自身的依赖发生了改变

function ProductPage({ productId }) {
  // ✅ 用 useCallback 包裹以避免随渲染发生改变
  const fetchProduct = useCallback(() => {
    // ... Does something with productId ...
  }, [productId]); // ✅ useCallback 的所有依赖都被指定了

  return <ProductDetails fetchProduct={fetchProduct} />;
}

function ProductDetails({ fetchProduct }) {
  useEffect(() => {
    fetchProduct();
  }, [fetchProduct]); // ✅ useEffect 的所有依赖都被指定了
  // ...
}

effect的依赖频繁变化(常见场景:定时器)

function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // effect在执行的时候 会创建一个闭包 并将count的值保存在该闭包中 且初始值为0 
    const id = setInterval(() => {
      setCount(count => count + 1); // setState函数更新形式
    }, 1000);
    return () => clearInterval(id);
  }, []); // effect不使用组件作用域中的任何变量

  return <h1>{count}</h1>;
}

Hook不常见现象解决方案

  • effect的依赖频繁变化

    在useEffect中,使用setState的函数式更新形式。它允许我们指定state该如何变化而不用引用当前state

  • 如何让effect首次不执行

    useRef存储一个布尔值来表示是首次渲染还是后续渲染

const hasMounted =  useRef(true) // 首次挂载
  // useEffect是按照顺序执行的
  useEffect(() => {
    if (hasMounted.current) {
      return 
    }
    console.log('11111', count, time)
  }, [count, time])

  useEffect(() => {
    hasMounted.current = false
  }, [])

编写一个自定义hooks来实现只在更新时执行effect

const useUpdateEffect = (effect, deps) => {
  const hasMounted = useRef(true) // 首次加载
  // effect首次加载不执行
  useEffect(() => {
    if (hasMounted.current) {
      return
    }
    return effect()
  }, deps);
  useEffect(() => {
    hasMounted.current = false
  }, [])
}

参考文档:

zh-hans.reactjs.org/docs/hooks-…