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
}, [])
}
参考文档: