跳到主要内容

5 篇博文 含有标签「hooks」

查看所有标签

自定义useSearchState与查询字符串同步状态

· 阅读需 1 分钟
TransonQ
Front-end Engineer

code​

代码: useSearchState-TransonQ

import { useState, useEffect } from "react";

function useSearchState<T>(key: string, init?: T) {
const searchParams = new URLSearchParams(window.location.search);
let initValue = searchParams.get(key) || "";

try {
JSON.parse(initValue); // 尝试解析查询字符串中的值
} catch (e) {
if (init !== undefined) {
initValue = JSON.stringify(init); // 如果解析错误,使用init的格式化后的值
} else {
initValue = ""; // 如果init也没有,初始化为空字符串
}
}

const [params, setParams] = useState<T>(() => {
return JSON.parse(initValue) as T;
});

useEffect(() => {
const searchParams = new URLSearchParams(window.location.search);
searchParams.set(key, JSON.stringify(params));
const newUrl = `${window.location.pathname}?${searchParams.toString()}`;
window.history.replaceState({}, "", newUrl);
}, [params, key]);

return [params, setParams] as const;
}

export default useSearchState;

使用案例​

https://codesandbox.io/p/sandbox/usesearchstate-key-init-9krjzz

使用useContext封装一个全局useToast

· 阅读需 3 分钟
TransonQ
Front-end Engineer
ToastContext.ts
import { createContext } from 'react'

interface ToastContextProps {
toast: {
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
}
}

export const ToastContext = createContext<ToastContextProps | undefined>(
undefined
)
emitter.ts
import mitt from 'mitt'

export const emitter = mitt()
GlobalToastProvider.tsx
import { Toast } from '@shopify/polaris'
import { useEffect, useMemo, useState } from 'react'
import { uniqueCode } from '../../utils'
import { ToastContext } from './ToastContext'
import { emitter } from './emitter'

/**
* Toasts 传参
*/
type ToastsProps = {
/** 前端生成的唯一id */
id: string
/** toast 内容 */
message: string
/** toast 类型 */
type: 'success' | 'error'
/** toast 持续时间 */
duration: number
/** 插入队列的时间 */
createdAt: Date
}

export const GlobalToastProvider = ({
children,
}: {
children: React.ReactNode
}) => {
/** toasts 队列 */
const [toasts, setToasts] = useState<ToastsProps[]>([])

/** 唤起成功的 toast */
const toastSuccess = (message: string, duration = 5000) => {
const newToast: ToastsProps = {
id: uniqueCode('toast_success'),
type: 'success',
message,
duration,
createdAt: new Date(), // 记录插入队列的时间
}

setToasts((prevToasts) => [...prevToasts, newToast])
}

/** 唤起错误的 toast */
const toastError = (message: string, duration = 5000) => {
const newToast: ToastsProps = {
id: uniqueCode('toast_error'),
type: 'error',
message,
duration,
createdAt: new Date(), // 记录插入队列的时间
}

setToasts((prevToasts) => [...prevToasts, newToast])
}

/** toast 唤起函数 */
const toastHandler = useMemo(() => {
return {
success: toastSuccess,
error: toastError,
}
}, [])

/** 移除 toast */
const dismissToast = (id: string) => {
setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id))
}

/** 定时器根据入队时间和 duration 比较确定 toast 存在时长 */
useEffect(() => {
let timer: NodeJS.Timeout
if (toasts.length === 0) return

timer = setInterval(() => {
const now = new Date()
const updatedToasts = toasts.filter((toast) => {
return now.getTime() - toast.createdAt.getTime() < toast.duration
})
setToasts(updatedToasts)
}, 1000)

return () => {
clearInterval(timer)
}
}, [toasts])

useEffect(() => {
emitter.on('toaster.error', (e: any) =>
toastHandler.error(e.message, e.duration)
)
emitter.on('toaster.success', (e: any) =>
toastHandler.success(e.message, e.duration)
)
return () => {
emitter.all.clear()
}
}, [toastHandler])

return (
<ToastContext.Provider value={{ toast: toastHandler }}>
{children}
{toasts.map((item) => (
<Toast
key={item.id}
content={item.message}
onDismiss={() => dismissToast(item.id)}
error={item.type === 'error'}
/>
))}
</ToastContext.Provider>
)
}
useToast
import { useContext } from 'react'
import { ToastContext } from './ToastContext'

type useToastValue = {
success: (message: string, duration?: number) => void
error: (message: string, duration?: number) => void
}

export const useToast = (): useToastValue => {
const context = useContext(ToastContext)

if (!context) {
throw new Error(
'useToast must be used within a <GlobalToastProvider> * </GlobalToastProvider>'
)
}

return context.toast
}
toaster.ts
import { emitter } from './emitter'

export const toaster = {
success: (message: string, duration?: number) => {
emitter.emit('toaster.success', { message, duration })
},
error: (message: string, duration?: number) => {
emitter.emit('toaster.error', { message, duration })
},
}

swr取消请求hook封装

· 阅读需 1 分钟
TransonQ
Front-end Engineer
function useCancellableSWR(key, swrOptions) {
const source = axios.CancelToken.source();

return [
useSWR(key, (url) => axios.get(url, { cancelToken: source.token }).then(res => res.data), {
...swrOptions,
}),
source,
];
}

// usage:
const [{ data }, cancelFn] = useCancellableSWR('/endpoint');

cancelFn.cancel()

issue-129-comment

useDebounceFn

· 阅读需 1 分钟
TransonQ
Front-end Engineer

useDebounceFn​

import { debounce } from 'lodash'
import { useMemo } from 'react'
import { useUnmount } from 'react-use'
import { useLatest } from './useLatest'

type DebounceOptions = {
// 间隔时间
wait?: number
leading?: boolean
trailing?: boolean
maxWait?: number
}

type useDebounceFnRetrunProps = {
run: () => void
cancel: () => void
flush: () => void
}

// ahooks 参数相同, 用法相同
export const useDebounceFn = (
fn: Function,
options?: DebounceOptions
): useDebounceFnRetrunProps => {
const fnRef = useLatest(fn)

const wait = options?.wait ?? 1000

const debounced = useMemo(
() =>
debounce(
(...args) => {
return fnRef.current(...args)
},
wait,
options
),
// 不能加依赖, 否则可能导致死循环
// eslint-disable-next-line
[]
)

useUnmount(() => {
debounced.cancel()
})

return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
}
}

useLatest​

import { useRef } from 'react'

export function useLatest<T>(value: T) {
const ref = useRef(value)
ref.current = value

return ref
}

useGlobalState

· 阅读需 2 分钟
TransonQ
Front-end Engineer

状态在多组件传递​

通常是:

  • 第三方库
  • context
  • props 传递

还可以手撸一个 useGlobalState

useGlobalState​

使用 react17 实现​

import { useState, useEffect, useCallback } from 'react';

export function createState(initialValue) {
return {
listeners: [],
state: initialValue,
};
}

export function useGlobalState(config) {
const [state, _setState] = useState(config.state);
const setState = useCallback(stateOrSetter => {
let next = stateOrSetter;
if (typeof stateOrSetter === 'function') {
next = stateOrSetter(config.state);
}
config.listeners.forEach(l => l(next));
config.state = next;
}, []);

useEffect(() => {
// Register the observer
const listener = state => _setState(state);
config.listeners.push(listener);

// Cleanup when unmounting
return () => {
const index = config.listeners.indexOf(listener);
config.listeners.splice(index, 1);
};
}, []);

return [state, setState];
}

使用:

const COUNT = createState(0);

const [count, setCount] = useGlobalState(COUNT);

使用 react 18 实现​

import { useCallback, useSyncExternalStore } from 'react';

export function createState(initialValue) {
return {
listeners: [],
state: initialValue,
};
}

export function useGlobalState(config) {
const setState = useCallback(stateOrSetter => {
let next = stateOrSetter;
if (typeof stateOrSetter === 'function') {
next = stateOrSetter(config.state);
}
config.state = next;
config.listeners.forEach(l => l());
}, []);

const state = useSyncExternalStore(
(listener) => {
config.listeners.push(listener);
return () => config.listeners.filter(l => l !== listener);
},
() => {
return config.state;
},
);
return [state, setState];
}

使用:

const COUNT = createState(0);

const [count, setCount] = useGlobalState(COUNT);