现如今,React 官方以不再推荐使用 class 组件,而是推崇 Function 组件,曾经的深层嵌套传值问题,也可以通过 Redux Took-kit + Hook 来进行轻松解决。以下,我们从多个实例,来总结学习一些,项目中通用的 hook 使用。
摘录的react hook使用场景:
- 状态管理:自定义 Hooks 可以用于封装状态管理逻辑,使多个组件能够共享和管理相同的状态。例如,可以创建一个自定义 Hooks 用于处理全局应用状态、用户身份验证状态或者表单字段的状态管理。
- 副作用处理:自定义 Hooks 可以封装处理副作用操作的逻辑,如数据订阅、网络请求、本地存储等。通过自定义 Hooks,可以在多个组件中共享副作用相关的代码,减少重复工作。例如,可以创建一个自定义 Hooks 用于处理数据获取、定时器操作或者订阅事件的逻辑。
- 数据获取和处理:自定义 Hooks 可以用于封装数据获取和处理的逻辑,以便在组件中使用。这样可以使组件更专注于渲染和交互的逻辑。例如,可以创建一个自定义 Hooks 用于从 API 中获取数据、对数据进行转换或者缓存数据。
- 表单处理:自定义 Hooks 可以用于处理表单的逻辑,包括表单校验、表单提交、表单重置等。通过自定义 Hooks,可以将表单相关的逻辑抽象出来,使得表单处理变得更简单和可复用。例如,可以创建一个自定义 Hooks 用于处理表单校验和提交逻辑。
- 定时器和动画效果:自定义 Hooks 可以用于处理定时器和动画效果的逻辑。通过自定义 Hooks,可以集中处理定时器相关的逻辑,实现定时器的启动、暂停、停止等操作。同样,可以封装常见的动画逻辑,使其在多个组件中可复用。
- 访问浏览器 API:自定义 Hooks 可以用于封装访问浏览器 API 的逻辑,如获取地理位置信息、访问本地存储、处理浏览器历史记录等。通过自定义 Hooks,可以在组件中方便地使用这些浏览器 API,提供更简洁的接口和更好的复用性。
- 复杂逻辑的封装:自定义 Hooks 还可以用于封装处理复杂逻辑的代码块,使其在多个组件中可复用。例如,可以创建一个自定义 Hooks 来处理分页逻辑、排序逻辑、权限控制逻辑等,从而避免在多个组件中重复编写这些逻辑。
这些仅是自定义 Hooks 的一些应用场景示例,实际上,自定义 Hooks 的应用范围非常广泛,几乎可以用于任何需要共享逻辑的情况。通过合理利用自定义 Hooks,我们可以提高代码的可维护性、可重用性和可读性,使得开发过程更加高效和愉悦。
1. React 自带 Hook
Hook | 作用 | 用法 |
---|---|---|
use | 读取 Promise 或 context 等资源的值,与 useContext 的区别:可以在 if 等循环中使用 | 循环中获取 context: if (isShow) { const ctx = use(ThemContext); } 读取 promie 数据(msgPromise 是接口请求): const content = use(msgPromise); |
useCallback | 在重新渲染之间缓存函数定义, 当 dependencies 变更时,返回新的函数 | useCallback(fn, dependencies) |
useContext | 从组件读取和订阅上下文 | const value = useContext(SomeContext) |
useDeferredValue | 推迟更新部分 UI,当新数据没有回来,则使用旧的数据进行显示 | const [query, setQuery] = useState(''); const deferredQuery = useDeferredValue(query); 代码中使用 deferredQuery |
useEffect | 用于处理组件中的副作用操作,当依赖变更时,return 函数将被执行 | useEffect(() => { const subscription = props.source.subscribe(); return () => { subscription.unsubscribe(); }; }, [props.source]); |
useLayoutEffect | useEffect 被异步调度,当页面渲染完成后再去执行,不会阻塞页面渲染。 useLayoutEffect 是在 commit 阶段新的 DOM 准备完成,但还未渲染到屏幕之前,同步执行 | 用法与 useEffect 一致 |
useId | 用于生成可以传递给可访问性属性的唯一 ID | const id = useId() |
useImperativeHandle | 在组件的顶层调用 useImperativeHandle 来定制它所暴露的 ref 句柄 | 详情看下面实例 |
useMemo | 缓存重新渲染之间的计算结果 | 组件内创建缓存数据:const visibleTodos = useMemo(() => filterTodos(todos, tab), [todos, tab]); 用于缓存组件: const Component = memo(({ items}) => { ...}) 组件内创建记忆函数,等同于 useCallback:const method = useMemo(() => { return () => { console.log('xxx')}}, [productId]) |
useOptimistic | 乐观地更新 UI | 详情见下面的实例 |
useReducer | 可以让你在组件中添加一个 reducer | 初始化:const [state, dispatch] = useReducer(reducer, initialArg, init?) 使用:dispatch({ type: 'incremented_age', vaule: 10 }) reducer: function reducer(state, action) { if (action.type === 'incremented_age') { return {age: state.age + Number(action.vaule)}; }} |
useRef | 引用渲染不需要的值,参数为 ref.current 属性的初始值,更改 current 属性不会触发渲染,dom 应用实例参考 useImperativeHandle 实例 | const intervalRef = useRef(0); const intervalId = setInterval(() => {}, 1000); intervalRef.current = intervalId; |
useState | 组件添加状态变量 | const [state, setState] = useState(initialState) |
useSyncExternalStore | 允许您订阅外部存储 | 没用过 |
useTransition | 在不阻塞 UI 的情况下更新状态 (没用过) | const [isPending, startTransition] = useTransition() |
1.1 use Hook
通过将 Promise 作为 prop 从服务器组件传递到客户端组件,可以将数据从服务器流式传输到客户端
javascript
// Parent Component: use hook
export default function App() {
const messagePromise = new Promise((resolve, reject) => {
reject();
}).catch(() => {
return "no new message found.";
});
return <MessageContainer messagePromise={messagePromise} />;
}
// use无法使用try-catch, 需要使用ErrorBoundary来捕获异常
export function MessageContainer({ messagePromise }) {
return (
<ErrorBoundary fallback={<p>⚠️Something went wrong</p>}>
<Suspense fallback={<p>⌛Downloading message...</p>}>
<Message messagePromise={messagePromise} />
</Suspense>
</ErrorBoundary>
);
}
// child Compnent
export function Message({ messagePromise }) {
const content = use(messagePromise);
return <p>Here is the message: {content}</p>;
}
1.2 useImperativeHandle 像父组件暴露方法
javascript
import { forwardRef, useImperativeHandle } from "react";
const MyInput = forwardRef(function MyInput(props, ref) {
const inputRef = useRef(null);
const customMethod = () => {
console.log("父组件可以调用");
inputRef.current.focus();
};
useImperativeHandle(
ref,
() => {
return {
customMethod,
};
},
[]
);
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.customMethod();
}
return (
<form>
<MyInput placeholder="Enter your name" ref={ref} />
<button type="button" onClick={handleClick}>
Edit
</button>
</form>
);
}
1.3 useOptimistic
可以再异步操作正在进行时,显示不同的状态。它接受某些状态为参数,并返回该状态参数的副本,该副本在异步操作(例如网络请求)期间可能不同,你提供一个函数,该函数接受当前状态和操作的输入,并返回操作待处理时要使用的乐观状态。会立即向用户呈现执行操作的结果,而该结果实际上是需要时间才能完成的。
javascript
// parent Component
export default function App() {
const [messages, setMessages] = useState([
{ text: "Hello there!", sending: false, key: 1 }
]);
async function sendMessage(formData) {
const sentMessage = await deliverMessage(formData.get("message"));
setMessages((messages) => [...messages, { text: sentMessage }]);
}
return <Thread messages={messages} sendMessage={sendMessage} />;
}
// useOptimistic 组件使用
function Thread({ messages, sendMessage }) {
const formRef = useRef();
async function formAction(formData) {
addOptimisticMessage(formData.get("message"));
formRef.current.reset();
await sendMessage(formData);
}
// const [optimisticState, addOptimistic] = useOptimistic(state, // updateFn
// (currentState, optimisticValue) => {
// // merge and return new state
// // with optimistic value
// }
// );
const [optimisticMessages, addOptimisticMessage] = useOptimistic(
messages, // messages: 异步中等待处理的值
(state, newMessage) => [ // 接受当前状态和传递给的乐观值addOptimisticMessage并返回结果乐观状态。它必须是一个纯函数。updateFn接受两个参数,返回新的值给optimisticMessages,当 `messages`处理完成,optimisticMessages将同步更新
...state,
{
text: newMessage,
sending: true,
other: 'other value'
}
]
);
return (
<>
{optimisticMessages.map((message, index) => (
<div key={index}>
{message.text}
{!!message.sending && <small> (Sending...)</small>}
</div>
))}
<form action={formAction} ref={formRef}>
<input type="text" name="message" placeholder="Hello!" />
<button type="submit">Send</button>
</form>
</>
);
}
2. 自定义 hook => 封装通用逻辑
这里整理一些常用的自定义 hook,在项目中都都可以通用的。
2.1 useCopyToClipboard Hook
使用
javascript
const { copiedText, copy } = useCopyToClipboard();
<>
<button
onClick={() => {
copy("A" + new Date().valueOf());
}}
>
copied me
</button>
<div>copied content: {copiedText}</div>
</>;
useCopyToClipboard 组件
javascript
import { useCallback, useState } from "react";
let textArea: any;
const useCopyToClipboard = () => {
const [copiedText, setCopiedText] = useState("");
const createTextArea = (text: string) => {
textArea = document.createElement("textArea");
textArea.readOnly = true;
textArea.contentEditable = "true";
textArea.value = text;
document.body.appendChild(textArea);
};
const isOS = useCallback(() => {
//can use a better detection logic here
return navigator.userAgent.match(/ipad|iphone/i);
}, []);
const selectText = useCallback(() => {
if (isOS()) {
const editable = textArea.contentEditable;
const readOnly = textArea.readOnly;
textArea.readOnly = "false";
textArea.focus();
textArea.select();
const range = document.createRange();
range.selectNodeContents(textArea);
const sel = window.getSelection();
if (!sel) return;
sel.removeAllRanges();
sel.addRange(range);
textArea.setSelectionRange(0, 999999);
textArea.contentEditable = editable;
textArea.readOnly = readOnly;
} else {
textArea.select();
}
}, []);
const copyTo = useCallback(() => {
document.execCommand("copy");
document.body.removeChild(textArea);
}, []);
const copyToClipboard = useCallback((text: string) => {
createTextArea(text);
selectText();
copyTo();
}, []);
const copy = useCallback((text: string) => {
setCopiedText(text);
// current method
if (navigator.clipboard) {
navigator.clipboard.writeText(text);
return;
}
// ie
if (window.clipboardData) {
window.clipboardData.clearData();
window.clipboardData.setData("Text", text);
return;
}
// deprecated
copyToClipboard(text);
}, []);
return {
copy,
copiedText,
};
};
export default useCopyToClipboard;
2.2 通用弹窗
使用
项目中会有很多弹窗,只是内容区域展示的内容不一样,可能是一段文字,可能是一个标案,可能是一个图片等等,同一个组件中,可以应用多个 Modal,使用方式如下:
javascript
const { open, Modal } = useModal();
const openModal = () => {
const time = new Date().valueOf();
open({
title: "custom" + time,
content: "hello",
cancelText: "cancel_btn",
isCancelBtn: true,
isOkBtn: false,
isMask: true,
});
};
// 内容将通过组件显示
<>
<Modal>custom content</Modal>
<button onClick={openModal}>open modal</button>
</>;
useModal
javascript
const initInfo = {
cancelText: "cancel_btn",
okText: "confirm_btn",
title: "",
content: "",
onCancel: undefined,
onOk: undefined,
isOkBtn: true,
isCancelBtn: true,
isMask: true,
};
const useModal = () => {
const [visiable, setVisiable] = useState(false);
const [info, setInfo] = useState < ModalProps > initInfo;
const open = useCallback(
(newInfo: ModalProps = {}) => {
setInfo((pre) => {
return { ...pre, ...newInfo };
});
setVisiable(true);
},
[info]
);
const close = useCallback(() => {
setInfo(initInfo);
setVisiable(false);
}, []);
const Modal = ({ children }: ModalProps): React.ReactNode => {
const [documentCanUse, setDocumentCanuse] = useState(false);
const {
cancelText = "",
okText = "",
title,
content,
onCancel,
onOk,
csClass,
isOkBtn,
isCancelBtn,
isMask,
} = info;
useEffect(() => {
setDocumentCanuse(true);
}, []);
const cancelEvent = useCallback(() => {
onCancel && onCancel();
close();
}, []);
const okEvent = useCallback(() => {
onOk && onOk();
close();
}, []);
const i1n8n = useLocaleContext().i18n;
if (!visiable) return null;
return (
<div>
{documentCanUse &&
createPortal(
<div className={isMask ? styles.modalContainer : ""}>
<div className={`${styles.modal} ${csClass}`}>
{title && <h2 className={styles.title}>{title}</h2>}
<div className={styles.content}>
{content}
{children}
</div>
{isCancelBtn && (
<button onClick={okEvent}>{i1n8n[cancelText]}</button>
)}
{isOkBtn && (
<button onClick={cancelEvent}>{i1n8n[okText]}</button>
)}
</div>
</div>,
document.body
)}
</div>
);
};
return {
Modal: memo(Modal),
open,
};
};
export default useModal;
2.3 集合 conext 实现全局 Notification
虽然也相当于是弹窗,但是不同于上面的 useModal, 在全局只能出现一个,用于提示信息,确认信息等。
注意:全局多国语也可以仿造该例子实现。
引入 NotificationProvider 和组件 NotificationBar
包裹在项目最外层,只需要应用一次
javascript
<NotificationProvider>
<NotificationBar />
<header className={styles.header}>header content</header>
<main className={styles.main}>{children}</main>
<footer className={styles.footer}>footer</footer>
</NotificationProvider>
使用
javascript
const notificationCtx = useContext(NotificationContext);
const openNotification = () => {
notificationCtx.open({
content: "Success: Project was fetched!-------------",
cancelText: "cancel_btn",
});
};
<button onClick={openNotification}>open </button>;
NotificationBar
javascript
const NotificationBar = () => {
const { notification, show } = useContext(NotificationContext);
const [documentCanUse, setDocumentCanuse] = useState(false);
const {
cancelText = "",
okText = "",
title,
content,
children,
onCancel,
onOk,
csClass,
isOkBtn,
isCancelBtn,
isMask,
} = notification;
useEffect(() => {
setDocumentCanuse(true);
}, []);
const i1n8n = useLocaleContext().i18n;
if (!show) return null;
return (
<div>
{documentCanUse &&
createPortal(
<div className={isMask ? styles.modalContainer : ""}>
<div className={`${styles.modal} ${csClass}`}>
{title && <h2 className={styles.title}>{title}</h2>}
<div className={styles.content}>
{content}
{children}
</div>
{isCancelBtn && (
<button onClick={onOk}>{i1n8n[cancelText]}</button>
)}
{isOkBtn && <button onClick={onCancel}>{i1n8n[okText]}</button>}
</div>
</div>,
document.body
)}
</div>
);
};
export default NotificationBar;
NotificationProvider
javascript
const initNotification = {
cancelText: "cancel_btn",
okText: "confirm_btn",
title: "",
content: "",
onCancel: undefined,
onOk: undefined,
isOkBtn: true,
isCancelBtn: true,
isMask: true,
};
const NotificationContext =
React.createContext <
NotificationContextType >
{ notification: initNotification, open: () => {}, show: false };
const NotificationProvider = (props: NotificationProps) => {
const [notification, setNotification] =
useState < NotificationProps > initNotification;
const [show, setShow] = useState(false);
const close = () => {
setShow(false);
};
const open = (noti: NotificationProps) => {
setNotification({
...notification,
...noti,
onOk: () => {
noti.onOk && noti.onOk();
close();
},
onCancel: () => {
noti.onCancel && noti.onCancel();
close();
},
});
setShow(true);
};
return (
<NotificationContext.Provider
value={{
open,
notification,
show,
}}
>
{props.children}
</NotificationContext.Provider>
);
};
export { NotificationProvider };
export default NotificationContext;
2.4 Form 表单通用验证
当项目中出现了许多表单,通常我们都需要对每一个表单字段进行字段初始化,例如 onChange, value 都能够绑定,有些还需要进行字段验证,或者再对表单字段是否更改做判断等等,那么可以提取一个通用的自定义 hook。
使用
在项目中引入 useForm Hook, 利用其功用方法,可以对 input, select, upload 等表单进行字段注入,校验。
javascript
const { register, handleSubmit, formState: { errors, data, isChange } } = useFrom(formValue);
render() {
return
<>
<input { ...register('lastName', { maxLength: 10, required: true, pattern: /^\d+$/ }) } />
{ errors?.lastName?.maxLength && <p>min length is 10</p> }
<Select { ...register('size', { required: true }) } options={ options } placeholder={ '请选择' } />
<Switch { ...register('isOpen') } />
<input { ...register('file', { required: true }) } />
{ errors?.file?.required && <p>File is required.</p> }
<Upload { ...register('file', undefined, { onChange: uploadEvent }) } />
<button onClick={ (e) => handleSubmit(e, onSubmit) }>提交</button>
</>
}
useForm
javascript
const useFrom = (defaultData: Data) => {
const [errors, setErrors] = useState<Errors>({})
const [data, setData] = useState<Data>(defaultData);
const [isChange, setIsChange] = useState(false);
const rules: Rules = {};
const validate = useCallback((key: string, rule: Rule, value: string | number) => {
const { required, pattern, minLength = 0, maxLength = 0 } = rule;
if (required !== undefined) {
const requiredError = required ? (value === '' || value === undefined || value === null || Number.isNaN(value as number)) : false;
setErrors({ ...errors, [key]: { required: requiredError } });
}
if (minLength > 0 && (String(value).length) < minLength) {
setErrors({ ...errors, [key]: { minLength: true } });
}
if (maxLength > 0 && (String(value).length) > maxLength) {
setErrors({ ...errors, [key]: { maxLength: true } });
}
if (pattern !== undefined && value !== '' && value !== undefined) {
const patternError = (!pattern?.test(String(value))) || false;
setErrors({ ...errors, [key]: { pattern: patternError } });
}
}
, [errors]);
const register = useCallback((key: string, rule?: Rule, extra?: Extra) => {
if (rule) {
rules[key] = rule;
}
return {
onChange: async (e: ChangeEvent<HTMLInputElement> | string | number) => {
let value: string | number = '';
value = (e as ChangeEvent<HTMLInputElement>).target ? (e as ChangeEvent<HTMLInputElement>).target.value : (e as string | number);
// upload address
value = await extra?.onChange(e as ChangeEvent<HTMLInputElement>) || value;
const newData = { ...data, [key]: value };
setData(newData);
rules[key] && validate(key, rules[key], value);
setIsChange(!isEqual(newData, defaultData));
},
onBlur: (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
rules[key] && validate(key, rules[key], value);
},
name: key,
value: data[key] || '',
}
}, [errors, data, rules]);
const handleSubmit = useCallback((e: MouseEvent<HTMLButtonElement>, onSubmit: () => void) => {
let pass = false;
Object.keys(rules).map(key => {
validate(key, rules[key], data[key]);
})
Object.keys(errors).map(key => {
if (!errors[key].required && !errors[key].pattern) {
pass = true;
}
})
if (pass) {
return onSubmit();
} else {
e.preventDefault();
e.stopPropagation();
}
}, [])
return {
register,
handleSubmit,
formState: { errors, data, isChange }
}
}
export default useFrom;
3. 自定义 Hook => 抽离业务逻辑
项目中,我结合了 redu-toolkit 的 createApi,因此当我在组件中获取数据的时候,才会有 loading 相关的数据,如果需要对 loading 时,进行 loading 状态的显示,那么,每个组件都需要进行重新写一次 loading 的判断:
javascript
const { isLoading, isError, data } = useGetQuotesQuery(numberOfQuotes);
if (isLoading) {
...
return;
}
if (isError) {
...
return;
}
render() {
data....
}
useLoadingHook
那么这就显得有点繁琐,因此这里我们将通用业务逻辑抽离为一个 hook。
javascript
export const useLoadingHook = <T extends {
data?: U,
isError: boolean,
isLoading: boolean,
}, U> (result: T) => {
const [isLoading, setLoading] = useState(result.isLoading);
const [isError, setIsError] = useState(result.isError);
const [documentCanUse, setDocumentCanuse] = useState(false);
useEffect(() => {
setLoading(result.isLoading);
setIsError(result.isError);
setDocumentCanuse(true);
}, [result]);
if (isError && documentCanUse) {
return (
<div>
{ createPortal(
<p className={ styles.modal }>There was an error!!!222</p>,
document.body
) }
</div>
);
}
if (isLoading && documentCanUse) {
return (
<div>
{ createPortal(
<p className={ styles.modal }>loading</p>,
document.body
) }
</div>
);
}
return '';
};
使用
javascript
const result = useGetQuotesQuery(numberOfQuotes);
const loadingInfo = useApiLoadingHook<typeof result, { quotes: Quote[]}>(result);
if (loadingInfo) return loadingInfo;
return <div className={ styles.container }>
content.....
</div>
4.自定义 hook => 常用工具
4.1 useDebounce(防抖)
javascript
import { useCallback, useRef } from "react";
const useDebounce = (fn, delay, argu) => {
const timer = useRef();
return useCallback(
(...args) => {
if (timer.current) {
clearTimeout(timer.current);
}
timer.current = setTimeout(() => {
fn && fn(...args);
}, delay);
},
[delay, fn, ...argu]
);
};
export default useDebounce;
4.2 useThrottle(节流)
javascript
import { useCallback, useRef } from "react";
const useThrottle = (fn: any, delay: any) => {
const timer: any = useRef();
return useCallback(() => {
if (timer.current) return;
timer.current = setTimeout(() => {
fn && fn();
timer.current = null;
}, delay);
}, [delay, fn]);
};
4.3 useResize (窗口 resize)
javascript
const useResize = () => {
const isWindow = typeof window !== "undefined";
const [size, setSize] = useState({
width: isWindow && window.innerWidth,
height: isWindow && window.innerHeight,
});
const method = useThrottle(() => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
}, 100);
useEffect(() => {
window.addEventListener("resize", method);
}, []);
return size;
};
4.4 useScroll (页面滚动)
页面滚动到固定位置,可以显示一个to top
的按钮等。
javascript
import { useState, useEffect } from "react";
const getPosition = () => {
return {
x: document.body.scrollLeft,
y: document.body.scrollTop,
};
};
const useScroll = () => {
const [position, setPosition] = useState(getPosition());
useEffect(() => {
const handler = () => {
setPosition(getPosition(document));
};
document.addEventListener("scroll", handler);
return () => {
document.removeEventListener("scroll", handler);
};
}, []);
return position;
};
useScroll 应用
javascript
import React, { useCallback } from "react";
import useScroll from "./useScroll";
function ScrollTop() {
const { y } = useScroll();
const goTop = useCallback(() => {
document.body.scrollTop = 0;
}, []);
if (y > 300) {
return (
<button
onClick={goTop}
style={{ position: "fixed", right: "10px", bottom: "10px" }}
>
Back to Top
</button>
);
}
return null;
}
5.自定义 hook => 拆分复杂组件
javascript
const useGetChildren = () => {
const { data } = useGetChldrenQuery(clasId);
const children = useMemo(() => {
return ...newChilren;
}, clasId);
return children;
};
const useGetTeachers = () => {
const { data } = useGetTeachersQuery(clasId);
const teachers = useMemo(() => {
return ...newCteachers;
}, clasId);
return teachers;
};
const ClassInfoCompnoent = () => {
const chilren = useGetChildren();
const teachers = useGetTeachers();
render() => {
......
}
}
当然,会有很多业务场景都可以通过hook来实现,让我们的代码粒度变得更小,更容易维护与测试,这都需要我们在开发中进行思考,加油。