React-hooks
React Hooks: let you use React without classes.(对于已有的使用class定义的React组件,官方不推荐全部重写。可将react hooks用于新创建的React组件)。
使用class定义React component有什么弊端:a. this指向不明确; b. 定义的handle函数需要bind
1. useState:接受的唯一参数为初始化的state值(仅在首轮render中被使用,不一定是object类型)。 返回一对参数:第一个为当前的state,第二个为其更新函数。
function ExampleWithManyStates() { // Declare multiple state variables! const [age, setAge] = useState(42); const [fruit, setFruit] = useState('banana'); const [todos, setTodos] = useState([{ text: 'Learn Hooks' }]); // ... }
这里,与使用class定义的component在写法上进行对比。
class Example extends React.Component { constructor (props) { super(props); this.setState = { age: 42, fruit: 'banana', todos: [{ text: 'React Hooks' }] }; } render () { return .... } }
2. useEffect(一个component中可以有多个useEffect)
何为effect(效应)?affect other components and can’t be done during rendering.如fetch data, set up a subscription, manually changing the DOM.
useEffect起到了与React classes中 componentDidMount
, componentDidUpdate
, componentWillUnmount相同的作用,但只不过是统一于一身。相当于通过useEffect传递了一个函数给React, React会在每次render之后(从浏览器渲染的角度讲,是在paint阶段之后)去调用effect函数(包括首轮render)。
import React, { useState, useEffect } from 'react'; function Example() { const [count, setCount] = useState(0); // Similar to componentDidMount and componentDidUpdate: useEffect(() => { // Update the document title using the browser API document.title = `You clicked ${count} times`; }); return ( <div> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> </div> ); }
当组件中含有subscribe时,通常在componentDidMount时添加subscribe,在componentWillUnmount时取消订阅。在这种情况下,需要及时进行clean up,以免造成内存泄漏。
import React, { useState, useEffect } from 'react'; function FriendStatus(props) { const [isOnline, setIsOnline] = useState(null); function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); // Specify how to clean up after this effect, when the component will unmount: return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
class写法如下:
class FriendStatus extends React.Component { constructor(props) { super(props); this.state = { isOnline: null }; this.handleStatusChange = this.handleStatusChange.bind(this); } componentDidMount() { ChatAPI.subscribeToFriendStatus( this.props.friend.id, this.handleStatusChange ); }
// 如果不加componentDidUpdate生命周期,可能会因为props改变导致内存泄漏。
componentDidUpdate(prevProps) {
// Unsubscribe from the previous friend.id
ChatAPI.unsubscribeFromFriendStatus(
prevProps.friend.id,
this.handleStatusChange
);
// Subscribe to the next friend.id
ChatAPI.subscribeToFriendStatus(
this.props.friend.id,
this.handleStatusChange
);
}
componentWillUnmount() { ChatAPI.unsubscribeFromFriendStatus( this.props.friend.id, this.handleStatusChange ); } handleStatusChange(status) { this.setState({ isOnline: status.isOnline }); } render() { if (this.state.isOnline === null) { return 'Loading...'; } return this.state.isOnline ? 'Online' : 'Offline'; } }
此外,useEffect还有第二个可选的参数(仅当该参数改变时,才去调用effect函数)
useEffect(() => { function handleStatusChange(status) { setIsOnline(status.isOnline); } ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange); }; }, [props.friend.id]); // Only re-subscribe if props.friend.id changes
当第二个参数为[]时,effect函数的调用不依赖于组建的任何props或state,即effect函数不会re-run.
3. 使用hooks的规则
- 只在顶层调用React hooks,不能用于循环、条件、内嵌函数中,保证每轮render中React hooks的调用顺序不变。(如果想要使用条件判断,要将其放在useEffect内部)
- 只在React function函数及自定义的hooks中调用hook函数
React提供的eslint-plugin-react-hooks默认包含在create-react-app中。
4. 使用自定义hooks,特点:1. 以use开头 2. 在其中调用其他hooks
作用: 重用状态逻辑(stateful logic)
import React, { useState, useEffect } from 'react'; function useFriendStatus(friendID) { const [isOnline, setIsOnline] = useState(null); // 传入props后,isOnline被设为true. function handleStatusChange(status) { setIsOnline(status.isOnline); } useEffect(() => { ChatAPI.subscribeToFriendStatus(friendID, handleStatusChange); return () => { ChatAPI.unsubscribeFromFriendStatus(friendID, handleStatusChange); }; }); // 返回 return isOnline; }
在两个组件中重用。
function FriendStatus(props) { const isOnline = useFriendStatus(props.friend.id); if (isOnline === null) { return 'Loading...'; } return isOnline ? 'Online' : 'Offline'; }
function FriendListItem(props) { const isOnline = useFriendStatus(props.friend.id); return ( <li style={{ color: isOnline ? 'green' : 'black' }}> {props.friend.name} </li> ); }
注意:自定义的hook仅仅用作状态逻辑的重用,并没有共享state。在重用过程中,每次调用都是独立的。
官网给出的另一个使用自定义hook的例子。其中,当recipientID改变时,自定义的useFriendStatus会解除对原来recipientID的订阅,并订阅新的recipientID。
const friendList = [ { id: 1, name: 'Phoebe' }, { id: 2, name: 'Rachel' }, { id: 3, name: 'Ross' }, ]; function ChatRecipientPicker() { const [recipientID, setRecipientID] = useState(1); const isRecipientOnline = useFriendStatus(recipientID); return ( <> <Circle color={isRecipientOnline ? 'green' : 'red'} /> <select value={recipientID} onChange={e => setRecipientID(Number(e.target.value))} > {friendList.map(friend => ( <option key={friend.id} value={friend.id}> {friend.name} </option> ))} </select> </> ); }
5. 其他内置hooks API
5.1 useContext: 参数必须是一个context对象(不能是cntext.provider或context.consumer),返回组件树中距离调用该API组件最近的Context.Provider的context value.当context value改变时,调用该API的组件将重新渲染。
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 theme = useContext(ThemeContext); return ( <button style={{ background: theme.background, color: theme.foreground }}> I am styled by theme context! </button> ); }
5.2 useReducer:是useState的替代,用于处理包含多个sub-value的复杂state逻辑,或者下一个state依赖于前一个。
const [state, dispatch] = useReducer(reducer, initialArg, init);
重写useState的count组件。这里,可以将初始化的state传入useReducer的第二个参数。
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); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
useReducer API还允许我们传入第三个参数——初始化函数。通过调用此函数init(initialArg)返回初始化的state.
function init(initialCount) { return {count: initialCount}; } function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; case 'reset': return init(action.payload); default: throw new Error(); } } function Counter({initialCount}) { const [state, dispatch] = useReducer(reducer, initialCount, init); return ( <> Count: {state.count} <button onClick={() => dispatch({type: 'reset', payload: initialCount})}> Reset </button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> <button onClick={() => dispatch({type: 'increment'})}>+</button> </> ); }
当返回的state与原来的state相等(Object.is)时,该组件的子组件不会重渲染,也不会触发effect.
5.3 useCallback: 当依赖[a,b]中的一个发生改变时,才会返回一个memorized callback。
const memoizedCallback = useCallback( () => { doSomething(a, b); }, [a, b], );
useCallback(fn, dep) 等价于 useMemo(() => fn, dep).
5.4 useMemo:返回一个memorized value。且只有当依赖[a,b]中的一个发生改变时,才会重新计算memorized value。(可用于性能优化,不必要的不会执行)
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
传递给useMemo的函数会在render过程中调用,这一点区别于useEffect中的函数。
如果没有第二个数组参数,每次渲染都会计算一个新的value.
5.5 useRef: 返回一个可变的ref对象,其current值由传入的参数initialValue进行初始化。
const refContainer = useRef(initialValue);
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { // `current` points to the mounted text input element inputEl.current.focus(); }; return ( <> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </> ); }
返回的ref对象的current属性发生改变时并不会触发re-render.
5.6 useLayoutEffect: 与useEffect基本相似,只是用在browser paint阶段之前。一般更推荐使用useEffect.