React Hooks -- useRef/useContext/useMemo/useCallback/自定义Hooks
useRef
useRef()返回一个具有current属性的对象,称为ref对象。把对象赋值给原生的React Element元素的ref属性,就能获取到对应的真实的DOM元素。
import React, { useRef } from "react"; const CustomTextInput = () => { const textInput = useRef(); // 创建ref对象 const focusTextInput = () => textInput.current.focus(); // 组件挂载完成,ref对象的current属性指向真实的DOM(input元素),调用focus方法 return (<> <input type="text" ref={textInput} /> <button onClick={focusTextInput}>Focus the text input</button> </>); }
把ref对象赋值给组件类型的React Element的ref属性时,也能获取到子组件,调用子组件中的方法,不过要配合使用forwardref和useImperativeHandle。forwardref把函数子组件包起来,组件就多接收了一个ref参数,把ref和要暴露出来的方法传递给useImperativeHandle。使用create-react-app 创建React项目,在src中创建一个Counter组件
import React, { forwardRef, useImperativeHandle } from 'react'; import { useState } from "react" // 组件被forwardRef之后,组件多接受一个ref参数 const Counter = (props, ref) => { const [count, setCount] = useState(0); const clickHandler = () => {setCount(c => c + 1);} // 第一个参数就是ref,第二个参数是函数,返回一个对象,父组件中ref对象的current属性就指向这个对象。 useImperativeHandle(ref, () => { return ({ click: clickHandler }) })
return <p>count is {count} </p> } export default forwardRef(Counter); // export forwardRef(组件)
App中使用ref,获取的子组件暴露出来的对象,并在button的click回调函数中使用
function App() { const counteRef = useRef(); const handleClick = () => {counteRef.current.click();} // ref对象获取到子组件暴露出来的对象。 return (<React.Fragment> <Counter ref={counteRef}></Counter> // ref对象赋值给子组件的ref属性。 <button onClick={handleClick}>Add</button> </React.Fragment>); }
如果项目中使用了Redux和React-Redux,connect中配置forwardRef: true
connect(null, null, null, { forwardRef: true })(组件);
ref对象是一个对象,属性可以保存任何值,useRef()可以接受一个参数,给属性赋初始值。有一个值在组件中,但它又与组件渲染无关,不是状态,不是属性,也不在JSX中使用,比如setTimeout返回的ID,这种值就可以放到ref中。更改ref的值,不会引起组件的重新渲染,因为值与渲染无关,并且在组件的整个生命周期中,ref对象一直存在。组件挂载,ref对象创建,组件销毁,ref对象销毁。
import React, { useRef, useEffect } from "react"; const Timer = () => { const intervalRef = useRef(); useEffect(() => { const id = setInterval(() => {console.log("A second has passed");}, 1000); intervalRef.current = id; return () => clearInterval(intervalRef.current); }); const handleCancel = () => clearInterval(intervalRef.current); return (<> //... </> ); }
需要注意的是更新ref对象的值,是一个副作用,因为它脱离React渲染过程,所以要放到useEffect或useLayoutEffect中,或放到事件处理函数中。
import React, { useRef } from "react"; const RenderCounter = () => { const counter = useRef(0); // counter.current = counter.current + 1; 不建议直接写在这里 // 建议放到useEffect中 useEffect(() => { counter.current = counter.current + 1; }); return ( <h1>{`The component has been re-rendered ${counter} times`}</h1> ); };
useContext
React Context使组件之间可以共享数据,但不用从父组件层层传递到子组件。Context提供了一个上下文(环境),只要在这个上下文中,子组件可以直接获取。Context.Provider包含起来的组件,就为组件提供了上下文,它的value(共享数据)就是可供子组件直接获取的数据(使用useContext获取)。React.createContext()创建一个context,它接受一个可选的参数,就是共享数据的默认值。比如登录之后,用户信息,页面的其他地方都要获取到,把用户信息放到Context中。create-react-app react-context 创建项目,userContext.js 创建context对象
import React from 'react'; export const UserContext = React.createContext()
App.js 中,Header组件用于获取用户信息,Detail用于显示信息,要设一个user状态和改变user的setUser,让这两个数据共享,所以把它们用Context包起来。
import React, {useState} from "react"; import Header from "./Header"; import Details from './Details'; import { UserContext } from './userContext'; export default function App() { const [user, setUser] = useState({}); return ( <UserContext.Provider value={{ user, setUser }}> <Header></Header> <Details></Details> </UserContext.Provider> ) }
Header和Detail都消费context中的数据,
import React, { useEffect, useContext } from "react"; import { UserContext } from './userContext'; export default function Header() { const { user, setUser } = useContext(UserContext); useEffect(() => { fetch('https://jsonplaceholder.typicode.com/users/1') .then(response => response.json()) .then(user => setUser(user)) }) return <div>用户名:{user.name}</div> } export default function Details() { const { user } = useContext(UserContext); return (<div>详细信息: {`${user.email} ${user.phone}`}</div> ); }
这有一个问题,Header中setUser时,App所有子组件都会重新渲染,能不能只渲染consumer组件?要使用children属性,children是一个特殊的属性,<A><B/> </A>,A组件包起来的B组件是A的children属性,它和A组件是同一个父组件。A组件怎么发生变化,B组件都不会变化。实现一个A组件,接受children属性,把要获取数据的子组件作为children传递给它。UserContext.js 添加
export const UserProvider = ({ children }) => { const [user, setUser] = useState(''); return <UserContext.Provider value={{ user, setUser }}> {children} </UserContext.Provider> }
App.js
import { UserProvider } from './userContext'; export default function App() { return ( <UserProvider> <Header></Header> <Details></Details> </UserProvider> ) }
在Header中调用setUser时,UserProvider组件会重新渲染,但UserProvider的children不会重新渲染,因为children是从props中获取的属性,没有改变。只有消费context的组件(children)才会重新渲染。context的consumer,当Provider的值改变时,consumer 重新渲染。Any components that consume the context, however, do re-render in response to the change of value on the provider, not because the whole tree of components has re-rendered.
随着共享数据越来越多,theme, user等,如果都写到一个value,如果value中有一个值改变,所有consumer都会重新渲染,没有必要。可以使用多个provider
function AppProvider({ children }) { // 可能存在状态 return ( <ThemeContext.Provider value="lava"> <UserContext.Provider value="sam"> <LanguageContext.Provider value="en"> {children} </LanguageContext.Provider> </UserContext.Provider> </ThemeContext.Provider > ); } // 使用它 <AppProvider> <App/> </AppProvider>
子组件,可以只想获取自己想要的值。
function InfoPage(props) { const theme = useContext(ThemeContext); const language = useContext(LanguageContext); return (/* UI */); } function Messages(props) { const theme = useContext(ThemeContext); const user = useContext(UserContext); // subscribe to messages for user return (/* UI */); }
组件之间简单共享数据另一种方式是Render props:就是属性可以被渲染(render)。属性可以是组件了,把整个组件作为属性进行传递,或者,属性是个函数,返回一个被渲染的组件。当属性是函数时,可以传一个data,渲染返回的组件时使用。比如Button组件有一个图标,为了能够更改图标,可能要提供size,width,color等参数,Button组件接受的参数越来越多,Button和Icon强绑定到一起,为什么不让图标整个组件传递过来?Button不管什么图标,给了,就渲染到合适的位置就行。
export function List({ data = [], renderItem, renderEmpty }) {
return data.length === 0
? (renderEmpty)
: (
<ul>
{data.map((item, i) => (
<li key={i}>{renderItem(item)}</li>
))}
</ul>
);
}
使用组件
<List
data={[{name: 'Rose', elevation: 2}]}
renderEmpty={<p>This list is empty</p>}
renderItem={item => {item.name} - {item.elevation.toLocaleString()}ft}
/>
useMemo/useCallback
React函数组件的最大问题就是,每次更新,组件就会重新渲染。里面的值就会重新计算,函数就会重新创建。函数通常会作为参数传递给子组件,又会引起子组件的重新渲染,导致没有必要的渲染。如果值不想重新计算,就要useMemo,记住函数的返回值。函数不想重新创建,就用useCallback包起来,它不会生成新函数,返回原函数。当然,它们都接受第二个参数,依赖数组,指定它们接受的函数的依赖,如果依赖有变化,值会重新计算并记住,函数会重新生成。子组件再配合使用React.memo,组件被调用的时候有相同的属性,子组件不会重复渲染。React.memo也可以单独使用,当一个组件渲染的时候,它的子组件也会渲染,不管子组件有没有变化,使用React.memo,子组件不会重新渲染。当把对象或数组作属性传递给子组件时,传递过去的是引用,所以才要求setState返回一个全新的对象,要不然传递过去的都是同一个引用,子组件不会更新。
import { useState } from "react"; function Items({ items }) { return ( <> <h2>Todo items</h2> <ul> {items.map((todo) => <li key={todo}>{todo}</li>)} </ul> </> ); } function Todo() { const [items, setItems] = useState(["Clean gutter", "Do dishes"]); const [newItem, setNewItem] = useState(""); const onSubmit = (evt) => { setItems((items) => items.concat([newItem])); setNewItem(""); evt.preventDefault(); }; const onChange = (evt) => setNewItem(evt.target.value); return ( <main> <Items items={items} /> <form onSubmit={onSubmit}> <input value={newItem} onChange={onChange} /> <button>Add</button> </form> </main> ); } function App() { return <Todo />;} export default App;
对于Items组件来说,items属性是引用。在输入框中输入数据,Items组件也会被渲染,只是因为父组件渲染,子组件一定会渲染,而不是因为items属性发生变化,此时Items引用并没有发生变化。只有点击Add按钮,setState返回了一个全新的数组,items属性指向了另外一个引用,这时才是因为属性发生了变化,子组件会重新渲染。输入时,items子组件是没有必要重复渲染的,所以可以用React.memo 包起来。但是当在Jsx中内联一个数组时,<Items items={['Complete to do list', ...items]},输入数据,父组件每次渲染都会重新创建一个数组,React.momeo 就不起作用了,这时用useMemo。
function Todo() {
// ...
const allItems = useMemo(() => ["Complete todo list"], [items])
return (
<main>
<Items items={allItems} />
// ...
</main>
);
}
In fact, useMemo doesn't do anything more than an assignment, except the assignment is conditional。如果Items 组件要删除,它需要接受函数参数,那就要把函数memorize.
const onDelete = useCallback((item) => {
setItems((list) => list.filter(i => i !== item))
}, [])
<Items items={allItems} onDelete ={onDelete} />
useMemo和useCallback只是返回一个值或函数,它和React的更新机制没有关系。
const Title = () => { const a = useMemo(() => { ... }, []) return <Child a={a} /> }
Title组件更新,Child组件一定更新,不管a的值有没有变化。
自定义hooks
使用hooks的函数组件鼓励你把相关副作用的逻辑放到一个地方。如果副作用是一个许多组件都需要的功能,可以把这段副作用的代码逻辑抽取出来,形成一个函数,这个函数就是自定义hook。比如做调查问卷的问题组件,都要加载问题,用户都要订阅,可以他们抽取到自定义hook中。和这些副作用相关的state,也要同步移动到相应的hook 中(Any state that is used solely for those 副作用 can be moved itno the corresponding hook)。
自定义hook可以维护自己的状态,它需要state来完成它的功能。由于hook 仅仅是函数,如果其他组件需要访问任意hook的state,hook可以把state 进行返回,包含到它的返回值中。比如一个自定义hook根据userId获取用户信息,它可以把获取到的用户数据store it locally,然后把它返回给调用hook的组件。每一个hook 封装自己的状态,就像其他函数一样。
const useToggle = (initialStatus = false) => { const [status, setStatus] = useState(initialStatus) const toggle = () => { setStatus(status => !status) } return [status, toggle] }
简单自定义fetch hook
import React, { useState, useEffect } from "react"; export function useFetch(uri){ const [data, setData] = useState(); const [error, setError] = useState(); const [loading, setLoading] = useState(true); useEffect(() => { if (!uri) return; fetch(uri).then(data => data.json()) .then(setData) .then(() => setLoading(false)) .catch(setError); }, [uri]); return { loading, data, error }; }
在useFetch的基础上,如果一个应用中,loading和error的处理方式都一样,完全可以封装出一个fetch组件出来。
function Fetch({ url, renderSucess, loadingFallback = <p>loading</p>, renderError = error => {<pre>{JSON.stringify(error)}</pre> } }) { const {loading, data, error} = useFetch(url); if(loading) return loadingFallback; if (error) return renderError(error); if (data) return renderSucess({data}); }
自定义hooks,当前组件是否已经挂载
export function useMountedRef() { const mounted = useRef(false); useEffect(() => { mounted.current = true; return () => (mounted.current = false); }); return mounted; }
当使用自定义hooks时,hooks内状态发生变化,使用hooks的组件会重新渲染(if the hook's state changes, the "host" component will re-render 相当于lifted state up),使用自定义hooks的组件一定小,只重复渲染它自己(把自定义hooks包装到小的组件中)。当在一个自定义hooks中,使用另一个hooks,hooks中状态变化会冒泡到包含最外层自定义hooks的组件。chaining hooks: if a hook’s state changes, it will cause its “host” hook change as well, which will propagate up through the whole chain of hooks until it reaches the “host” component and re-renders it (which will cause another chain reaction of re-renders, only downstream now)。一个自定义hooks,一个state,不相干的state放到不同的自定义hooks中。一个自定义hooks也不用一个不想干的自定义hooks,一个自定义hooks做一件事。