一、初步了解
-
Hook 是 React 16.8 的新增特性。
-
亮点:
- 可以让你在不编写 class
(Hook 在 class 内部是不起作用的。)
的情况下使用 state 以及其他的 React 特性。 - Hook 使你在无需修改组件结构的情况下复用状态逻辑。
- Hook 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。
- 可以让你在不编写 class
-
Hook 使用规则
-
只能在函数最外层调用 Hook。不要在循环、条件判断或者子函数中调用。
if (name !== '') { useEffect(function persistForm() { localStorage.setItem('formData', name); }); }
-
只能在 React 的函数组件中调用 Hook,(还有自定义的Hook中)。不要在普通的 JavaScript 函数中调用。
-
-
注意点:
思考?可以使用多个useEffect 那么 React 怎么知道哪个 state 对应哪个
useState
?答案是 React 靠的是 Hook 调用的顺序。只要 Hook 的调用顺序在多次渲染之间保持一致,React 就能正确地将内部 state 和对应的 Hook 进行关联。
function Form() { // 1. Use the name state variable const [name, setName] = useState('Mary'); // 2. Use an effect for persisting the form useEffect(function persistForm() { localStorage.setItem('formData', name); }); // 3. Use the surname state variable const [surname, setSurname] = useState('Poppins'); // 4. Use an effect for updating the title useEffect(function updateTitle() { document.title = name + ' ' + surname; }); // ... }
// ------------ // 首次渲染 // ------------ useState('Mary') // 1. 使用 'Mary' 初始化变量名为 name 的 state useEffect(persistForm) // 2. 添加 effect 以保存 form 操作 useState('Poppins') // 3. 使用 'Poppins' 初始化变量名为 surname 的 state useEffect(updateTitle) // 4. 添加 effect 以更新标题 // ------------- // 二次渲染 // ------------- useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略) useEffect(persistForm) // 2. 替换保存 form 的 effect useState('Poppins') // 3. 读取变量名为 surname 的 state(参数被忽略) useEffect(updateTitle) // 4. 替换更新标题的 effect
为什么不在if条件语句中写hook?
// 在条件语句中使用 Hook 违反不在if条件语句中规则
if (name !== '') {
useEffect(function persistForm() {
localStorage.setItem('formData', name);
});
}
```js
// 如果这是hook执行顺序,由于判断条件,name 为false 的时候就不执行了,这是顺序就错位了,后面的会提前执行,产生bug
useState('Mary') // 1. 读取变量名为 name 的 state(参数被忽略)
// useEffect(persistForm) // 🔴 此 Hook 被忽略!
useState('Poppins') // 🔴 2 (之前为 3)。读取变量名为 surname 的 state 失败
useEffect(updateTitle) // 🔴 3 (之前为 4)。替换更新标题的 effect 失败
```
正确的写法
```js
useEffect(function persistForm() {
// 👍 将条件判断放置在 effect 中
if (name !== '') {
localStorage.setItem('formData', name);
}
});
```
二、概括
-
useState
与 class 组件中的
setState
方法不同,useState
不会自动合并更新对象。我们都知道useState接受一个初始值,这个初始值如果是动态获取的话可以:
const [state, setState] = useState(() => { const initialState = someExpensiveComputation(props); return initialState; });
-
useEffect
useEffect 会在每次渲染后都执行。
effect 需要清除: 在useEffect 中 返回一个函数,React 将会在执行清除操作时调用它。React 会在组件卸载的时候执行清除操作。
useEffect( () => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source], );
-
useReducer
可以让你通过 reducer 来管理组件本地的复杂 state。
useState
的替代方案。它接收一个形如(state, action) => newState
的 reducer,并返回当前的 state 以及与其配套的dispatch
方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。)state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用
useReducer
还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递dispatch
而不是回调函数 。-
useReducer 的 state 初始化有两种方法
-
指定初始 state将初始: state 作为第二个参数传入
useReducer
是最简单的方法 -
const [state, dispatch] = useReducer( reducer, {count: initialCount} );
-
惰性初始化: 需要将
init
函数作为useReducer
的第三个参数传入,这样初始 state 将被设置为init(initialArg)
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: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } // 这么做可以将用于计算 state 的逻辑提取到 reducer 外部,这也为将来对重置 state 的 action 做处理提供了便利
-
-
-
useRef
useRef
返回一个可变的 ref 对象,其.current
属性被初始化为传入的参数(initialValue
)。返回的 ref 对象在组件的整个生命周期内保持不变。本质上,
useRef
就像是可以在其.current
属性中保存一个可变值的“盒子”。useRef
会在每次渲染时返回同一个 ref 对象。注意:当 ref 对象内容发生变化时,
useRef
并不会通知你。变更.current
属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。
三、自定义Hook
-
应知概念:
-
自定义 Hook 是一种自然遵循 Hook 设计的约定,而并不是 React 的特性。
-
**自定义 Hook 必须以 “use” 开头吗? ** 必须如此。
这个约定非常重要。不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 的规则。
-
在两个组件中使用相同的 Hook 会共享 state 吗? 不会。
自定义 Hook 是一种重用状态逻辑的机制,所以每次使用自定义 Hook 时,其中的所有 state 和副作用都是完全隔离的。
-
自定义 Hook 如何获取独立的 state? 每次调用 Hook,它都会获取独立的 state。
我们可以在一个组件中多次调用
useState
和useEffect
,它们是完全独立的。
-
四、mobx 与 hook
For React, we get official bindings via the mobx-react
package. But for hooks, we need to use another library mobx-react-lite. This gives us custom hooks with which we can create observables directly in our components.
对于React,我们通过mobx-react包获得官方绑定。但是对于hook,我们需要使用另一个库mobx-react-lite。这为我们提供了自定义钩子,我们可以使用它直接在组件中创建可观察对象。
-
observer(componentClass)
Function that converts a function component into a reactive component, which tracks which observables are used automatically re-renders the component when one of these values changes. Observables can be passed through props, accessed from context or created locally with
useObservable
.将函数组件转换为反应性组件的函数,当其中一个值发生更改时,该函数跟踪使用哪些可观察值自动重新呈现组件。可观察对象可以通过道具传递,可以通过上下文访问,也可以使用useObservable在本地创建。
-
Observer
Observer
is a React component, which appliesobserver
to an anonymous region in your component. It takes as children a single, argumentless function which should return exactly one React component. The rendering in the function will be tracked and automatically re-rendered when needed. This can come in handy when needing to pass render function to external components (for example the React Native listview), or if you dislike theobserver
function.Observer是一个React组件,它将Observer应用于组件中的匿名区域。它将一个没有参数的函数作为子函数,该函数应该只返回一个React组件。函数中的呈现将被跟踪,并在需要时自动重新呈现。当需要将呈现函数传递给外部组件(例如React Native listview),或者不喜欢observer函数时,这可以派上用场。
import { Observer } from "mobx-react-lite" function ObservePerson(props) { const person = useObservable({ name: "John" }) return ( <div> {person.name} <Observer>{() => <div>{person.name}</div>}</Observer> <button onClick={() => (person.name = "Mike")}>No! I am Mike</button> </div> ) }
// In case you are a fan of render props, you can use that instead of children. Be advised, that you cannot use both approaches at once, children have a precedence. Example import { Observer } from "mobx-react-lite" function ObservePerson(props) { const person = useObservable({ name: "John" }) return ( <div> {person.name} <Observer render={() => <div>{person.name}</div>} /> <button onClick={() => (person.name = "Mike")}>No! I am Mike</button> </div> ) }
observer<P>(baseComponent: FunctionComponent<P>, options?: IObserverOptions): FunctionComponent<P>
-
useObservable
React hook that allows creating observable object within a component body and keeps track of it over renders. Gets all the benefits from observable objects including computed properties and methods. You can also use arrays and Map which are useful to track dynamic list/table of information. The Set is not supported (see https://github.com/mobxjs/mobx/issues/69).
React hook允许在组件体中创建可观察对象,并在呈现过程中跟踪它。从可观察对象获得所有好处,包括计算属性和方法。您还可以使用数组和映射来跟踪动态信息列表/表。不支持该设置(请参见https://github.com/mobxjs/mobx/issues/69)。
警告:对于当前实现,还需要将组件包装到observer,否则更新时的重新运行程序将不会发生。
import { observer, useObservable } from "mobx-react-lite" const TodoList = observer(() => { const todos = useObservable(new Map<string, boolean>()) const todoRef = React.useRef() const addTodo = React.useCallback(() => { todos.set(todoRef.current.value, false) todoRef.current.value = "" }, []) const toggleTodo = React.useCallback((todo: string) => { todos.set(todo, !todos.get(todo)) }, []) return ( <div> {Array.from(todos).map(([todo, done]) => ( <div onClick={() => toggleTodo(todo)} key={todo}> {todo} {done ? " " : " ⏲"} </div> ))} <input ref={todoRef} /> <button onClick={addTodo}>Add todo</button> </div> ) })
useObserver<T>(fn: () => T, baseComponentName = "observed", options?: IUseObserverOptions): T
五、总结
- hooks每次 Render 都有自己的 Props 与 State 可以认为每次 Render 的内容都会形成一个快照并保留下来(函数被销毁了,但变量被react保留下来了),因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。 这也是函数式的特性–数据不变性
- 性能注意事项 useState 函数的参数虽然是初始值,但由于整个函数都是 Render,因此每次初始化都会被调用,如果初始值计算非常消耗时间,建议使用函数传入,这样只会执行一次:
- 如果你熟悉 React 的 class 组件的生命周期,你可以认为
useEffect Hook
就是组合了componentDidMount, componentDidUpdate, 以及 componentWillUnmount(在useEffect的回调中)
,但是又有区别,useEffect不会阻止浏览器更新屏幕 - hooks 把相关的逻辑放在一起统一处理,不在按生命周期把逻辑拆分开
- useEffect 是在浏览器 render 之后触发的,想要实现 DOM 改变时同步触发的话,得用 useLayoutEffect,在浏览器重绘之前和 DOM 改变一起,同步触发的。不过尽量在布局的时候,其他的用标准的 useEffect,这样可以避免阻止视图更新。
六、参考资料
[hook原理]
[《怎么用 React Hooks 造轮子》](https://segmentfault.com/a/1190000017057144)
[探讨1.]
[探讨2.]
七、分享案例
-
useState
import React, { useState } from "react"; // 这里只想在强调一下HOOK。通过在函数组件里调用它来给组件添加一些内部state。React 会在重复渲染时保留这个 state。useState 会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并。 export const UseStateCom = () => { // 一个组件中可以多次使用S塔特Hook; const [count, setCount] = useState(2); const [todos, setTodos] = useState("banana"); return ( <div style={{ height: "100px", width: "100%", background: "pink", marginTop: 30 }}> <h1>useState</h1> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> <button onClick={() => setTodos( "good luck" + count)}>{todos}</button> </div> ); }; // Capture Value 概念的解释:每次 Render 的内容都会形成一个快照并保留下来,因此当状态变更而 Rerender 时,就形成了 N 个 Render 状态,而每个 Render 状态都拥有自己固定不变的 Props 与 State。 export const UseStateComCase = () => { const [temp, setTemp] = useState(5); const log = () => { setTimeout(() => { console.log("3 秒前 temp = 5,现在 temp =", temp); }, 3000); }; return ( <button onClick={() => { log(); setTemp(3); // 3 秒前 temp = 5,现在 temp = 5 }} > 3 秒前 temp = 5,现在 temp = ? </button> ); // 在 log 函数执行的那个 Render 过程里,temp 的值可以看作常量 5,执行 setTemp(3) 时会交由一个全新的 Render 渲染,所以不会执行 log 函数。而 3 秒后执行的内容是由 temp 为 5 的那个 Render 发出的,所以结果自然为 5。 };
-
useEffect
import React, { useState, useEffect } from "react"; // useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。 export const UseEffectCom = () => { const [count, setCount] = useState(0); const [state, setState] = useState({ left: 30, top: 20, width: 100, height: 100 }); // 相当于 componentDidMount 和 componentDidUpdate: useEffect(() => { // 使用浏览器的 API 更新页面标题 document.title = `You clicked ${count} times`; }); useEffect(() => { function handleWindowMouseMove(e) { // 展开 「...state」 以确保我们没有 「丢失」 width 和 height setState(state => ({ ...state, left: e.pageX, top: e.pageY })); } // 注意:这是个简化版的实现 window.addEventListener("mousemove", handleWindowMouseMove); // 回收机制 return () => window.removeEventListener("mousemove", handleWindowMouseMove); }, []); // [count]: 仅在 count 更改时更新 // 传入了一个空数组([]),effect 内部的 props 和 state 就会一直拥有其初始值。 return ( <div style={{ height: "220px", width: "100%", background: "pink", marginTop: 30 }}> <h1>useEffect</h1> <p>You clicked {count} times</p> <button onClick={() => setCount(count + 1)}> Click me </button> { Object.keys(state).map((e, i) => ( <h4 key={i}>{state[e]}</h4> )) } </div> ); }; export const CounterUseEffect = () => { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }); return <h1>{count}</h1>; } // setInterval 我们只想执行一次,所以我们自以为聪明的向 React 撒了谎,将依赖写成 []。 // “组件初始化执行一次 setInterval,销毁时执行一次 clearInterval,这样的代码符合预期。” 你心里可能这么想。 // 但是你错了,由于 useEffect 符合 Capture Value 的特性,拿到的 count 值永远是初始化的 0。相当于 setInterval 永远在 count 为 0 的 Scope 中执行,你后续的 setCount 操作并不会产生任何作用。 export const UseEffectComTest = () => { // 1. Use the name state variable const [name, setName] = useState("Mary"); // 2. Use an effect for persisting the form if (name !== "") { useEffect(function persistForm() { localStorage.setItem("formData", name); }); } // 3. Use the surname state variable const [surname, setSurname] = useState("Poppins"); // 4. Use an effect for updating the title useEffect(function updateTitle() { document.title = name + " " + surname; }); return ( <div style={{ height: "220px", width: "100%", background: "pink", marginTop: 30 }}> <h1>UseEffectComTest</h1> <p>name: {name}</p> <p>surname: {name}</p> <button onClick={() => setName("")}> Click me </button> </div> ); };
-
useRef
import React, { useState, useRef, useCallback } from "react"; // useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内保持不变。 // 如何绕过 Capture Value // 利用 useRef 就可以绕过 Capture Value 的特性。可以认为 ref 在所有 Render 过程中保持着唯一引用,因此所有对 ref 的赋值或取值,拿到的都只有一个最终状态,而不会在每个 Render 间存在隔离。 export const UseRefCom = () => { const [inputValue, setValue] = useState("good luck"); const [height, setHeight] = useState(0); const inputEl = useRef(null); const onButtonClick = () => { // `current` 指向已挂载到 DOM 上的文本输入元素 inputEl.current.focus(); console.log("inputEl.current--", inputEl.current.value); console.log("inputEl--", inputEl); }; // 注意,当 ref 对象内容发生变化时,useRef 并不会通知你。变更 .current 属性不会引发组件重新渲染。如果想要在 React 绑定或解绑 DOM 节点的 ref 时运行某些代码,则需要使用回调 ref 来实现。 const measuredRef = useCallback(node => { if (node !== null) { setHeight(node.getBoundingClientRect().height); } }, []); const getInputValue = useRef(null); return ( <div style={{ height: "250px", width: "100%", background: "pink", marginTop: 30 }}> <h1>useRef</h1> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button><br/> <h1 ref={measuredRef}>Hello, world</h1> <h2>The above header is {Math.round(height)}px tall</h2> <input ref={getInputValue} type="text" defaultValue={inputValue} onChange={e => setValue(e.target.value)} /> <h2>{inputValue}</h2> </div> ); };
-
自定义Hook
import React, { useState, useEffect, useRef, useCallback } from "react"; // 自定义Hook 练习 export const Counter = () => { const [count, setCount] = useState<any>(0); const prevCountRef = useRef(); useEffect(() => { prevCountRef.current = count; }); const prevCount = prevCountRef.current; return ( <div style={{ height: "100px" }}> <input onChange={e => setCount(e.target.value)} /> <h1>Now: {count}, before: {prevCount}</h1> </div> ); }; const usePrevious = (value) => { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }; export const CounterHook = () => { const [count, setCount] = useState<any>(0); const prevCount = usePrevious(count); return ( <div style={{ height: "100px", background: "pink", marginTop: "30px" }}> <input onChange={e => setCount(e.target.value)} /> <h1>Now: {count}, before: {prevCount}</h1> </div> ); }; // 之前获取高的案例的封装 export const useClientRect = () => { const [rect, setRect] = useState(null); const ref = useCallback(node => { if (node !== null) { setRect(node.getBoundingClientRect()); } }, []); return [rect, ref]; }; export const MeasureExample = () => { const [rect, ref] = useClientRect(); return ( <div style={{ height: "100px", background: "pink", marginTop: "30px" }}> <h1 ref={ref}>Hello, world</h1> {rect !== null && <h2>The above header is {Math.round(rect.height)}px tall</h2> } </div> ); };
-
useReducer
import React, { useReducer } from "react"; // useState 的替代方案。它接收一个形如 (state, action) => newState 的 reducer,并返回当前的 state 以及与其配套的 dispatch 方法。(如果你熟悉 Redux 的话,就已经知道它如何工作了。) // 通过dispatch对应的action来修改状态,而状态的修改由统一的reducer来处理 export const reducer = (state, action) => { switch (action.type) { case "increment": return {count: state.count + 1}; case "decrement": return {count: state.count - 1}; default: throw new Error(); } }; export const UseReducerCom = ({initialState}) => { const [state, dispatch] = useReducer(reducer, initialState); return ( <div style={{ marginTop: 30 }}> <h1>useReducer</h1> Count: {state.count} <button onClick={() => dispatch({type: "increment"})}>+</button> <button onClick={() => dispatch({type: "decrement"})}>-</button> </div> ); };
import React, { useReducer } from "react"; // action是一个对象 export const initialState = { count1: 0, count2: 0, }; export const reducer = (state, action) => { switch (action.type) { case "increment1": return { ...state, count1: state.count1 + 1 }; case "decrement1": return { ...state, count1: state.count1 - 1 }; case "set1": return { ...state, count1: action.count }; case "increment2": return { ...state, count2: state.count2 + 1 }; case "decrement2": return { ...state, count2: state.count2 - 1 }; case "set2": return { ...state, count2: action.count }; default: throw new Error("Unexpected action"); } }; // 在 state中存放了两个数字。我们能使用复杂的对象表示 state,只要能把 reducer组织好(列如:react-redux中 combineReducers)。另外,因为 action是一个对象,除了 type值,你还可以给它添加其他属性像action.count。这个例子中 reducer 是有点杂乱的,但不妨碍我们下面这样使用它 export const UseReducerMore = () => { const [state, dispatch] = useReducer(reducer, initialState); const [state1, dispatch2] = useReducer(reducer, initialState); return ( <div> <div> <p>{state.count1}</p> <button onClick={() => dispatch({ type: "increment1" })}>+1</button> <button onClick={() => dispatch({ type: "decrement1" })}>-1</button> <button onClick={() => dispatch({ type: "set1", count: 0 })}>reset</button> </div> <div> <p>{state1.count2}</p> <button onClick={() => dispatch2({ type: "increment2" })}>+1</button> <button onClick={() => dispatch2({ type: "decrement2" })}>-1</button> <button onClick={() => dispatch2({ type: "set2", count: 0 })}>reset</button> </div> </div> ); };
页面中直接引入组件即可
import React, { Component } from "react"; interface TestState { count: number; } class App extends Component<{}, TestState> { constructor(props) { super(props); this.state = { count: 10, }; } componentDidMount() { document.title = `You clicked ${this.state.count} times`; } componentDidUpdate() { // 如果没有这个,count 除了第一次设置,是不改变的。 // document.title = `You clicked ${this.state.count} times`; } render() { const initialState = {count: 0}; return ( <div> <SearchHeader /> {/* <UseStateCom /> <UseStateComCase /> */} {/* 和 UseEffectCom 做对比, 验证 useEffect 的作用 */} {/* <div> <p>You clicked {this.state.count} times</p> <button onClick={() => this.setState({ count: this.state.count + 1 })}> Click me </button> </div> */} {/* 由上下比较 得出 useEffect 每次 state 更新都会触发。 */} {/* <UseEffectCom /> */} {/* 第二参数为[] 的问题 */} {/* <CounterUseEffect /> */} {/* 反向验证useEffect 的注意事项 */} {/* <UseEffectComTest /> */} {/* useRef */} {/* <UseRefCom /> */} {/* useReducer */} <UseReducerCom initialState={initialState} /> <UseReducerMore /> {/* 自定义Hook */} {/* <Counter /> <CounterHook /> <MeasureExample /> */} </div> ); } } export default App;