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做一件事。

  

posted @ 2022-03-09 12:20  SamWeb  阅读(4888)  评论(3编辑  收藏  举报