likes
comments
collection
share

React学习整理(一) --hooks

作者站长头像
站长
· 阅读数 59

React 内置hooks

Hook 可以帮助在组件中使用不同的 React 功能。你可以使用内置的 Hook 或使用自定义 Hook。本页列出了 React 中所有内置 Hook。

State Hook

useSate

用于给组件添加状态

1. 普通用法

可以设置初始值,通过结构的方式获取[变量,修改变量的函数]

import { useState } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  function handleClick() {
    console.log(count);  // 0
    setCount(count + 1);
    console.log(count); // 0
  }

  return (
    <button onClick={handleClick}>
      You pressed me {count} times
    </button>
  );
}

注意: set函数调用后,直接获取,得到的还是调用前的值。只影响下一次useState返回的内容。

2. 根据上一个state更新state

只需要把set函数中内容设置为函数即可

function handleClick() {  
setAge(age + 1); // setAge(42 + 1)  
setAge(age + 1); // setAge(42 + 1)  
setAge(age + 1); // setAge(42 + 1)  
} // 结果为43

// 使用下面这种方式
function handleClick() {  
setAge(a => a + 1); // setAge(42 => 43)  
setAge(a => a + 1); // setAge(43 => 44)  
setAge(a => a + 1); // setAge(44 => 45)  
}// 结果为45

3. 更新对象和数组

在 React 中,状态被认为是只读的,因此

方法一:你应该替换它而不是改变现有对象

// 🚩 不要像下面这样改变一个对象:
form.firstName = 'Taylor';

// ✅ 使用新对象替换 state  
setForm({  
...form,  
firstName: 'Taylor'  
});

setPerson({
      ...person,
      name: e.target.value
    });
     setTodos([
      ...todos,
      {
        id: nextId++,
        title: title,
        done: false
      }
    ]);

方法二:可以使用Immer

先安装immer库

import { useImmer } from 'use-immer';

let nextId = 3;
const initialList = [
  { id: 0, title: 'Big Bellies', seen: false },
  { id: 1, title: 'Lunar Landscape', seen: false },
  { id: 2, title: 'Terracotta Army', seen: true },
];

export default function BucketList() {
  const [list, updateList] = useImmer(initialList);

  function handleToggle(artworkId, nextSeen) {
    updateList(draft => {
      const artwork = draft.find(a =>
        a.id === artworkId
      );
      artwork.seen = nextSeen;
    });
  }

  return (
    <>
      <h1>Art Bucket List</h1>
      <h2>My list of art to see:</h2>
      <ItemList
        artworks={list}
        onToggle={handleToggle} />
    </>
  );
}

function ItemList({ artworks, onToggle }) {
  return (
    <ul>
      ...
    </ul>
  );
}

4. 更新函数

// ❌
const [fn, setFn] = useState(someFunction);  
function handleClick() {  
setFn(someOtherFunction);  
}

// ✅
const [fn, setFn] = useState(() => someFunction);  
function handleClick() {  
setFn(() => someOtherFunction);  
}

👎不建议写法

1. 在初始化状态,传入函数调用结果

尽管 createInitialTodos() 的结果仅用于初始渲染,但你仍然在每次渲染时调用此函数。如果它创建大数组或执行昂贵的计算,这可能会浪费资源。

请注意,你传递的是 createInitialTodos 函数本身,而不是 createInitialTodos() 调用该函数的结果。如果将函数传递给 useState,React 仅在初始化期间调用它。

function TodoList() { 
    // 👎不建议
    const [todos, setTodos] = useState(createInitialTodos());
    
    // 👍建议,传入函数名
    const [todos, setTodos] = useState(createInitialTodos);
}

useReducer

useReducer同样是可以用来修改状态的,它更像是把一个state的各种更新的行为,放到一个函数来处理。

const [state, dispatch] = useReducer(reducer, initialArg, init?)
  • state是变量,用于渲染。
  • dispatch是函数,通常需要传入一个对象(通常称为action),通常有一个type属性
  • reducer是一个函数,有两个参数,一个是state,另一个是dispatch中传入的action,并返回新的state
  • initialArg是作为初始化参数,如果init函数存在,则state初始化参数为init(initialArg),否则是initialArg
import { useReducer } from 'react';

function reducer(state, action) {
  switch (action.type) {
    case 'incremented_age': {
      return {
        name: state.name,
        age: state.age + 1
      };
    }
    case 'changed_name': {
      return {
        name: action.nextName,
        age: state.age
      };
    }
  }
  throw Error('Unknown action: ' + action.type);
}

const initialState = { name: 'Taylor', age: 42 };

export default function Form() {
  const [state, dispatch] = useReducer(reducer, initialState);

  function handleButtonClick() {
    dispatch({ type: 'incremented_age' });
  }

  function handleInputChange(e) {
    dispatch({
      type: 'changed_name',
      nextName: e.target.value
    }); 
  }

  return (
    <>
      <input
        value={state.name}
        onChange={handleInputChange}
      />
      <button onClick={handleButtonClick}>
        Increment age
      </button>
      <p>Hello, {state.name}. You are {state.age}.</p>
    </>
  );
}



Context Hook

上下文帮助组件 从祖先组件接收信息,而无需将其作为 props 传递

useContext

通过createContext()创建Xxxcontext上下文,使用<Xxxcontext.Provider>进行包裹,在后面的子孙组件中,通过useContext(Xxxcontext)获取到内容

import { createContext, useContext, useState } from 'react';

const ThemeContext = createContext(null);
const CurrentUserContext = createContext(null);

export default function MyApp() {
  const [theme, setTheme] = useState('light');
  return (
    <MyProviders theme={theme} setTheme={setTheme}>
      <WelcomePanel />
      <label>
       ...
      </label>
    </MyProviders>
  );
}

function MyProviders({ children, theme, setTheme }) {
  const [currentUser, setCurrentUser] = useState(null);
  return (
    <ThemeContext.Provider value={theme}>
      <CurrentUserContext.Provider
        value={{
          currentUser,
          setCurrentUser
        }}
      >
        {children}
      </CurrentUserContext.Provider>
    </ThemeContext.Provider>
  );
}

function WelcomePanel({ children }) {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    ...
  );
}

function Greeting() {
  const {currentUser} = useContext(CurrentUserContext);
  return (
    <p>You logged in as {currentUser.name}.</p>
  )
}

function LoginForm() {
  const {setCurrentUser} = useContext(CurrentUserContext);
  
  return (
    <>
     ...
    </>
  );
}

function Panel({ title, children }) {
  const theme = useContext(ThemeContext);
  const className = 'panel-' + theme;
  return (
   ...
  )
}

function Button({ children, disabled, onClick }) {
  const theme = useContext(ThemeContext);
  const className = 'button-' + theme;
  return (
    ...
  );
}



Ref Hook

ref 允许组件 保存一些不用于渲染的信息,比如 DOM 节点或 timeout ID。与状态不同,更新 ref 不会重新渲染组件。ref 是从 React 范例中的“脱围机制”。当需要与非 React 系统如浏览器内置 API 一同工作时,ref 将会非常有用。

useRef

它能帮助引用一个不需要渲染的值。

【特点】

  • 不会触发重复渲染
  • 带有current属性,可以即时更新,常用来作为不需要渲染的一些变量
const ref = useRef(initialValue)

function handleStartClick() {  
const intervalId = setInterval(() => {  
// ...  
}, 1000);  
intervalRef.current = intervalId;  
}

ref操作dom

import { useRef } from 'react';  
function MyComponent() {  
const inputRef = useRef(null);  
// ...
// ...  
return <input ref={inputRef} />;

当 React 创建 DOM 节点并将其渲染到屏幕时,React 将会把 DOM 节点设置为 ref 对象的 current 属性。现在可以借助 ref 对象访问 <input> 的 DOM 节点,并且可以调用类似于 focus() 的方法:

function handleClick() {  
    inputRef.current.focus();  
}

useImperativeHandle

可以用来自定义由 ref 暴露出来的句柄

例如下方,给父组件暴露focus和scrollIntoView方法

import { forwardRef, useRef, useImperativeHandle } from 'react';

const MyInput = forwardRef(function MyInput(props, ref) {
  const inputRef = useRef(null);

  useImperativeHandle(ref, () => {
    return {
      focus() {
        inputRef.current.focus();
      },
      scrollIntoView() {
        inputRef.current.scrollIntoView();
      },
    };
  }, []);

  return <input {...props} ref={inputRef} />;
});
import { useRef } from 'react';
import MyInput from './MyInput.js';

export default function Form() {
  const ref = useRef(null);

  function handleClick() {
    ref.current.focus();
    // 下方代码不起作用,因为 DOM 节点并未被暴露出来:
    // ref.current.style.opacity = 0.5;
  }

  return (
    <form>
      <MyInput placeholder="Enter your name" ref={ref} />
      <button type="button" onClick={handleClick}>
        Edit
      </button>
    </form>
  );
}

Effect Hook

Effect 允许组件 连接到外部系统并与之同步。这包括处理网络、浏览器、DOM、动画、使用不同 UI 库编写的小部件以及其他非 React 代码。

有些组件需要与网络、某些浏览器 API 或第三方库保持连接,当它们显示在页面上时。这些系统不受 React 控制,所以称为外部系统。

import { useEffect } from 'react';
import { createConnection } from './chat.js';

function ChatRoom({ roomId }) {
  const [serverUrl, setServerUrl] = useState('https://localhost:1234');

  useEffect(() => {
  	const connection = createConnection(serverUrl, roomId);
    connection.connect();
  	return () => {
      connection.disconnect();
  	};
  }, [serverUrl, roomId]);
  // ...
}

useEffect

Effect 是从 React 范式中的“脱围机制”。避免使用 Effect 协调应用程序的数据流。如果不需要与外部系统交互,那么 可能不需要 Effect

  • 如果你的 Effect 确实没有使用任何响应式值,则它仅在 初始渲染后 运行。
useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  }, []);
  • 不设置依赖项,Effect 会在组件的 每次单独渲染(和重新渲染)之后 运行。
useEffect(() => {
    const connection = createConnection(serverUrl, roomId);
    connection.connect();
    return () => {
      connection.disconnect();
    };
  });

在 Effect 中根据先前 state 更新 state

// ❌
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1); // 你想要每秒递增该计数器...
    }, 1000)
    return () => clearInterval(intervalId);
  }, [count]); // 🚩 ... 但是指定 `count` 作为依赖项总是重置间隔定时器。
  // ...
}


// 👍
import { useState, useEffect } from 'react';

export default function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(c => c + 1); // ✅ 传递一个 state 更新器
    }, 1000);
    return () => clearInterval(intervalId);
  }, []); // ✅现在 count 不是一个依赖项

  return <h1>{count}</h1>;
}

useLayoutEffect

useLayoutEffectuseEffect 的一个版本,在浏览器重新绘制屏幕之前触发。

大多数组件不需要依靠它们在屏幕上的位置和大小来决定渲染什么。他们只返回一些 JSX,然后浏览器计算他们的 布局(位置和大小)并重新绘制屏幕。

有时候,这还不够。想象一下悬停时出现在某个元素旁边的 tooltip。如果有足够的空间,tooltip 应该出现在元素的上方,但是如果不合适,它应该出现在下面。为了让 tooltip 渲染在最终正确的位置,你需要知道它的高度(即它是否适合放在顶部)。

要做到这一点,你需要分两步渲染:

  1. 将 tooltip 渲染到任何地方(即使位置不对)。
  2. 测量它的高度并决定放置 tooltip 的位置。
  3. 把 tooltip 渲染放在正确的位置。

所有这些都需要在浏览器重新绘制屏幕之前完成。你不希望用户看到 tooltip 在移动。调用 useLayoutEffect 在浏览器重新绘制屏幕之前执行布局测量:

import { useRef, useLayoutEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import TooltipContainer from './TooltipContainer.js';

export default function Tooltip({ children, targetRect }) {
  const ref = useRef(null);
  const [tooltipHeight, setTooltipHeight] = useState(0);

  useLayoutEffect(() => {
    const { height } = ref.current.getBoundingClientRect();
    setTooltipHeight(height);
    console.log('Measured tooltip height: ' + height);
  }, []);

  let tooltipX = 0;
  let tooltipY = 0;
  if (targetRect !== null) {
    tooltipX = targetRect.left;
    tooltipY = targetRect.top - tooltipHeight;
    if (tooltipY < 0) {
      // 它不适合上方,因此把它放在下面。
      tooltipY = targetRect.bottom;
    }
  }

  return createPortal(
    <TooltipContainer x={tooltipX} y={tooltipY} contentRef={ref}>
      {children}
    </TooltipContainer>,
    document.body
  );
}


useInsertionEffect

调用 useInsertionEffect 在任何可能需要读取布局的副作用启动之前插入样式:

let isInserted = new Set();
function useCSS(rule) {
  useInsertionEffect(() => {
    // 同前所述,我们不建议在运行时注入 <style> 标签。
    // 如果你必须这样做,那么应当在 useInsertionEffect 中进行。
    if (!isInserted.has(rule)) {
      isInserted.add(rule);
      document.head.appendChild(getStyleForRule(rule));
    }
  });
  return rule;
}

性能Hook

useMemo

useMemo 是一个 React Hook,它在每次重新渲染的时候能够缓存计算的结果。

import { useMemo } from 'react';

function TodoList({ todos, tab }) {
  const visibleTodos = useMemo(
    () => filterTodos(todos, tab),
    [todos, tab]
  );
  // ...
}

useCallback

useCallback 是一个允许你在多次渲染中缓存函数的 React Hook。

import { useCallback } from 'react';

export default function ProductPage({ productId, referrer, theme }) {
  const handleSubmit = useCallback((orderDetails) => {
    post('/product/' + productId + '/buy', {
      referrer,
      orderDetails,
    });
  }, [productId, referrer]);

useTransition

useTransition 是一个帮助你在不阻塞 UI 的情况下更新状态的 React Hook。

简单理解就是,降低渲染优先级,让其他高优先级的内容先渲染。例如:图表组件在渲染时,在输入框中输入内容,那么输入框中的内容优先渲染,图表组件的可以后置渲染。

function TabContainer() {
  const [isPending, startTransition] = useTransition();
  const [tab, setTab] = useState('about');

  function selectTab(nextTab) {
    startTransition(() => {
      setTab(nextTab);
    });
  }
  // ……
}
  1. isPending,告诉你是否存在待处理的 transition。
  2. startTransition 函数,你可以使用此方法将状态更新标记为 transition。

传递给 startTransition 的函数必须是同步的。React 会立即执行此函数,并将在其执行期间发生的所有状态更新标记为 transition。如果在其执行期间,尝试稍后执行状态更新(例如在一个定时器中执行状态更新),这些状态更新不会被标记为 transition。

在Transition中更新父组件

你也可以通过调用 useTransition 以更新父组件状态。例如,TabButton 组件在 Transition 中包装了 onClick 逻辑

export default function TabButton({ children, isActive, onClick }) {
  const [isPending, startTransition] = useTransition();
  if (isActive) {
    return <b>{children}</b>
  }
  return (
    <button onClick={() => {
      startTransition(() => {
        onClick();
      });
    }}>
      {children}
    </button>
  );
}

useDefferredValue

useDeferredValue 是一个 React Hook,可以让你延迟更新 UI 的某些部分。

import { useState, useDeferredValue } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  // ...
}

如果是直接使用query值,则每次输入框有内容变化,则显示loading,而使用useDeferedValue,则会先保留更新前的值,之后再更新最新的值(可能会有点看起来像卡顿的感觉)

一个常见的备选 UI 模式是 延迟 更新结果列表,并继续显示之前的结果,直到新的结果准备好。调用 useDeferredValue 并将延迟版本的查询参数向下传递:

import { Suspense, useState, useDeferredValue } from 'react';
import SearchResults from './SearchResults.js';

export default function App() {
  const [query, setQuery] = useState('');
  const deferredQuery = useDeferredValue(query);
  return (
    <>
      <label>
        Search albums:
        <input value={query} onChange={e => setQuery(e.target.value)} />
      </label>
      <Suspense fallback={<h2>Loading...</h2>}>
        <SearchResults query={deferredQuery} />
      </Suspense>
    </>
  );
}

其他Hook

useDebugValue,useId,useSyncExternalStore,useActionState

转载自:https://juejin.cn/post/7376097985379926052
评论
请登录