跳到主要内容

react-router 6.8.0离开路由确认自定义弹窗

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

需求​

自从上一篇博客react-router-dom v6.3 切换路由前确认提到的6版本破坏性更新和暂时废除了相关的api, 目前已经可以升级到 6.8.0 版本了,在此可以使用其提供的新暂定api解决自定义离开路由时候询问用户是否离开。

核心 : unstable_useBlocker​

在react-router-dom 6.8.0的源码里面unstable_useBlocker类型如下:

export declare function useBlocker(shouldBlock: boolean | BlockerFunction): Blocker;

深入查看unstable_useBlocker的返回值类型:

type Blocker = BlockerUnblocked | BlockerBlocked | BlockerProceeding;

// 锁定路由时
interface BlockerBlocked {
state: "blocked";
reset(): void;
proceed(): void;
location: Location;
}
// 未锁定路由时
interface BlockerUnblocked {
state: "unblocked";
reset: undefined;
proceed: undefined;
location: undefined;
}
// 离开路由的过程中
interface BlockerProceeding {
state: "proceeding";
reset: undefined;
proceed: undefined;
location: Location;
}

要点

  • 传入接受一个布尔值
  • 返回值根据三种状态分为三个不同的类型
  • 主要关注在BlockerBlocked时候, reset 和 proceed 是一个明确的函数

组件设计​

import { Modal } from '@shopify/polaris'
import { useEffect } from 'react'
import { unstable_useBlocker } from 'react-router-dom'
import { useToggle } from 'react-use'

type NavBlockerProps = {
isBlocked: boolean
}

export const NavBlocker = ({ isBlocked }: NavBlockerProps) => {
const blocker = unstable_useBlocker(isBlocked)

const [active, toggleActive] = useToggle(false)

const confirmLeave = () => {
toggleActive(false)
blocker.proceed?.()
}
const cancel = () => {
toggleActive(false)
blocker.reset?.()
}

useEffect(() => {
if (blocker.state === 'blocked' || blocker.state === 'proceeding') {
toggleActive()
}
}, [blocker, toggleActive])

useEffect(() => {
if (blocker.state === 'blocked' && !isBlocked) {
toggleActive()
blocker.reset()
}
}, [blocker, isBlocked, toggleActive])

return (
<Modal
title="确认离开"
open={active}
onClose={cancel}
primaryAction={{
content: '确认离开',
destructive: true,
onAction: confirmLeave,
}}
secondaryActions={[{ content: '取消', onAction: cancel }]}
>
<Modal.Section>确认离开当前页面?</Modal.Section>
</Modal>
)
}

常用下载函数

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

链接下载​

export function downLoadFile(url: string, filename: string, blank = true) {
if (!isURL(url)) return
let downloadLink = document.createElement('a')
downloadLink.download = filename
downloadLink.href = url
blank && (downloadLink.target = '_blank')
downloadLink.click()
window.URL.revokeObjectURL(downloadLink.href)
}

blob 下载​

需要设置 responseType: 'blob'

axios 案例

export const postDownloadLetter = () =>
axios.request({
method: 'post',
url: 'url',
responseType: 'blob',
})

代码

export const downloadBlob = (blob: any, fileName: string) => {
const elink = document.createElement('a')
elink.download = fileName
elink.style.display = 'none'
elink.href = URL.createObjectURL(blob)
document.body.appendChild(elink)
elink.click()
URL.revokeObjectURL(elink.href)
document.body.removeChild(elink)
}

prettier 格式化指定范围文件

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

格式化src中所有 js, jsx 文件​

要在项目中使用 Prettier 格式化指定范围内的文件,可以使用以下步骤:

  1. 在项目中安装 Prettier。可以使用 npm 或者 yarn 进行安装。例如,使用 npm 安装:
npm install --save-dev prettier
  1. 在项目的根目录下创建一个 .prettierrc 文件,用来配置 Prettier 的格式化规则。可以参考 Prettier 的文档来配置规则。

  2. 在项目中安装一个可以格式化指定范围内文件的工具,例如 prettier-eslint-cli 或者 prettier-standard-cli。安装命令如下:

npm install --save-dev prettier-eslint-cli

或者

npm install --save-dev prettier-standard-cli
  1. 在项目的 package.json 文件中添加一个脚本命令,用来格式化指定范围内的文件。例如,添加以下脚本:
"scripts": {
"format": "prettier-eslint --write \"src/**/*.{js,jsx}\""
}

这个脚本会格式化所有在 src 目录下的 .js 和 .jsx 文件。

  1. 运行脚本命令来格式化指定范围内的文件。例如,在命令行中运行以下命令:
npm run format

这个命令会执行 prettier-eslint 工具来格式化指定范围内的文件,并将格式化后的内容写回到原文件中。

注意:在使用 prettier-eslint-cli 或者 prettier-standard-cli 工具时,需要根据自己的项目配置来选择合适的工具。如果项目中使用了 ESLint 或者 StandardJS 等代码规范工具,那么可以选择 prettier-eslint-cli 或者 prettier-standard-cli 工具来保证代码格式化的一致性。

忽略指定路径文件​

如果你想要在 format 脚本中排除 src/api 文件夹里面的所有文件,你可以使用 ! 符号来排除指定的文件或目录。具体来说,你可以将脚本修改为以下形式:

"format": "prettier-eslint --write \"src/**/*.{js,jsx}\" --ignore-path .prettierignore"

然后,在项目的根目录下创建一个 .prettierignore 文件,并将需要排除的文件或目录添加到这个文件中。例如,以下内容会排除 src/api 文件夹下的所有文件:

src/api/**

这样,当你运行 npm run format 命令时,prettier-eslint 将会自动忽略 src/api 文件夹下的所有文件,并格式化其它指定范围内的文件。

注意,如果你在项目中同时使用了 ESLint 和 Prettier,那么你可能需要在项目中配置 ESLint 的 Prettier 插件,并在 .prettierignore 文件中包含 ESLint 忽略的文件或目录。具体配置方法可以参考 ESLint 和 Prettier 的文档。如果你想要在 format 脚本中排除 src/api 文件夹里面的所有文件,你可以使用 ! 符号来排除指定的文件或目录。具体来说,你可以将脚本修改为以下形式:

如果你想要添加更多的排除规则,可以在 .prettierignore 文件中添加更多的内容。例如,以下内容会同时排除 src/api 和 src/test 文件夹下的所有文件:

src/api/**
src/test

doctor报错AS找不到Java版本

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

更新Android Studio (version 2022.1)后doctor报错​

# [Unable to find bundled Java version [duplicate]](https://stackoverflow.com/questions/75115909/flutter-android-studio-version-2022-1-x-unable-to-find-bundled-java-versio)

flutter-doctor报错

解决步骤 - MAC​

一 : 找到引用程序,点击右键显示包内容​

找到AS包点击右键

二: 复制 jbr , 粘贴副本改名成 jre​

AS内部复制jbr副本改名jre

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);

react-router-6.5支持可选路由段

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

可选路径参数​

在 path 结尾添加?可以使得整个路由参数变为可选的,比如:

<Route path=":lang?/about"> 

将会匹配为:

  • /:lang/about
  • /about
<Route path="/multistep/:widget1?/widget2?/widget3?">

将会匹配为:

  • /multistep
  • /multistep/:widget1
  • /multistep/:widget1/:widget2
  • /multistep/:widget1/:widget2/:widget3

可选的静态路径参数​

<Route path="/home?">

将会匹配为:

  • /
  • /home
<Route path="/fr?/about">
  • /about
  • /fr/about

React + TS最小上手指南

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

前端开发的最小上手指南

类型​

基本类型​

string // e.g. "Pat"
boolean // e.g. true
number // e.g. 23 or 1.99

数组​

number[] // e.g. [1, 2, 3]

string[] // e.g. ["Lisa", "Pat"]

User[] // custom type e.g. [{ name: "Pat" }, { name: "Lisa" }]

对象​

type User = {
firstName: string;
age: number;
isNice: boolean;
role: string;
skills: string[];
friends?: User[]; // 字段可选
}

使用 type 声明的类型约束对象的写法:

const tom: User = { ... }
// 或者
const jerry = { ... } as User

枚举​

顾名思义就是约束在指定值范围之间

enum ProductStatus {
DRAFT = "draft",
PUBLISHED = "published",
}

type Product = {
name: string;
price: number;
images: string[];
status: ProductStatus;
};

const product = {
name: "Shampoo",
price: 2.99,
images: ["image-1.png", "image-2.png"],
status: ProductStatus.PUBLISHED,
} as Product;

函数​

函数的传参类型​

直接传参

function fireUser(firstName: string, age: number, isNice: boolean) {
...
}

// 箭头函数
const fireUser = (firstName: string, age: number, isNice: boolean) => {
...
}

对象传参直接定义类型

function fireUser({ firstName, age, isNice }: {
firstName: string;
age: number;
isNice: boolean;
}) {
...
}

对象传参抽取类型

type User = {
firstName: string;
age: number;
role: UserRole;
}

function fireUser({ firstName, age, role }: User) {
...
}

// 箭头函数
const fireUser = ({ firstName, age, role }: User) => {
...
}

函数返回值类型​

可以直接在函数入参的小括号后面加上 : 类型 的方式

function fireUser(firstName: string, age: number, role: UserRole): User {
// 逻辑部分
return { firstName, age, role };
}

// 箭头函数
const fireUser = (firstName: string, age: number, role: UserRole): User => {
...
}

这强制返回正确的类型。如果不是声明的类型,编译器变报错,如下所示

如果不希望严格返回指定类型, 可以不写函数返回类型, TS 自行推断返回值。TS 位置越少月好,如果不清楚返回类型,可以将鼠标悬停到函数名为止,查看推断类型。

可能会遇到的概念​

React + TS​

在 react 中,使用上述 ts 知识通常是足够的,react 的函数组件和 hooks 就是一系列简单函数,组件传参 props 就是一个对象。在大部分案例中,甚至都不需要定义类型,ts 可以自动推断类型。

函数组件​

在大多数情况下,不需要定义函数组件的返回值类型,只需要定义 props 对象中接收的传参类型。很多不接收 props 的函数组件不需要定义任何类型。

function UserProfile() {
return <div>If you're Pat: YOU'RE AWESOME!!</div>
}

通常 ts 项目会推断该函数组件返回值类型为JSX.Element

TS 的方便之处:如果我们在返回了一个非 JSX 的元素,TS 会通过报错提示我们组件返回的不是一个 JSX 元素。

Props​

enum UserRole {
CEO = "ceo",
CTO = "cto",
SUBORDINATE = "inferior-person",
}

type UserProfileProps = {
firstName: string;
role: UserRole;
}

function UserProfile({ firstName, role }: UserProfileProps) {

if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}
return <div>Hi {firstName}, you suck!</div>

}

注意: 您会发现很多代码使用 React.FC 或 React.FunctionComponent 来声明组件。不过,不再推荐这样做了。

回调函数​

在项目中,我们经常会传递回调函数作为 props 。如下案例:

type UserProfileProps = {
id: string;
firstName: string;
role: UserRole;
// 回调函数的类型
fireUser: (id: string) => void; // void 作为返回类型的时候, 表示无
};

function UserProfile({ id, firstName, role, fireUser }: UserProfileProps) {

if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>;
}

return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser(id)}>Fire this loser!</button>
</>
);
}

默认 props​

对于 TS 中的可选类型,组件的 porps 中的属性同样可以定义可选的属性。

type UserProfileProps = {
age: number;
role?: UserRole;
}

当组件需要设定一个默认值且是可选的时候,可以解构 props 然后分配到需要默认值的属性上面:

function UserProfile({ firstName, role = UserRole.SUBORDINATE }: UserProfileProps) {

if (role === UserRole.CTO) {
return <div>Hey Pat, you're AWESOME!!</div>
}

return <div>Hi {firstName}, you suck!</div>
}

useState 自动推断类型​

使用最多的 hook 就是useState,但在大部分场景中是不需要为其定义类型的,只需要设置好初始值,ts 可以推断此状态的类型。

在tsx中,如果使用setState方法时传入与初始值不同类型,那么 ts 将会报错。

useState 手动添加类型​

在某些特定的场景中,ts 不能推断类型的默认值(比如:数组默认值 [] ,无法得知数组元素类型)

const [names, setNames] = useState([]); // 元素类型不明确

const [user, setUser] = useState(); // 无法推断是什么类型

上述的例子中:如果直接调用 setNames([ 'Tom', 'Lisa' ]) 时会报错: 不能指定类型。

useState是用泛型实现的,可以传入我们指定的状态类型来手动定义状态类型

// names 状态的类型是 string[] ,这样我们就明确了该状态的类型.
const [names, setNames] = useState<string[]>([]);

setNames(["Pat", "Lisa"]);

// user 的类型是 User | undefined (可以设置为一个 user 或者 undefined)
const [user, setUser] = useState<User>();

setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(undefined);

// user 的类型是 User | null (可以设置为一个 user 或者 null)
const [user, setUser] = useState<User | null>(null);

setUser({ firstName: "Pat", age: 23, role: UserRole.CTO });
setUser(null);

以上在项目中使用useState就足够了,另外注意:useEffect 不需要类型。

自定义 hooks​

前面也说到:hooks 就是一个普通函数。那么:自定义 hooks 自然也是函数。定义类型的方式同函数。

function useFireUser(firstName: string) {
const [isFired, setIsFired] = useState(false);
const hireAndFire = () => setIsFired(!isFired);

return {
text: isFired ? `Oops, hire ${firstName} back!` : "Fire this loser!",
hireAndFire,
};
}

function UserProfile({ firstName, role }: UserProfileProps) {
const { text, hireAndFire } = useFireUser(firstName);

return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={hireAndFire}>
{text}
</button>
</>
);
}

React 事件​

使用内联事件函数不需要定义任何类型,ts 已经知道内联事件回调的默认入参 event。

function FireButton() {
return (
<button onClick={(event) => event.preventDefault()}>
Fire this loser!
</button>
);
}

当单独定义事件处理函数时候,刚开始可能会带来kunhuo:

function FireButton() {
const onClick = (event: ???) => { // 这是什么类型?
event.preventDefault();
};

return (
<button onClick={onClick}>
Fire this loser!
</button>
);
}

想知道单独生命的事件处理函数的时候,我们获取这个 event 的类型有两种方式:

  • 谷歌...
  • 先写一个内联回调,将鼠标悬停到内联回调的 event 上,根据编辑器的类型提示,类型拿来吧你。

咱甚至都不需要理解发生了什么,c&v 就完事了。就是我们在 useStata了解到的泛型。

function FireButton() {
const onClick = (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
};
return (
<button onClick={onClick}>
Fire this loser!
</button>
);
}

我们用同样的方式,可以获取到输入事件的 event 类型:

function Input() {
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.value);
};
return <input onChange={onChange} />;
}

select 事件的 event

function Select() {
const onChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
console.log(event.target.value);
};

return <select onChange={onChange}>...</select>;
}

children的类型/组件的类型​

type LayoutProps = {
children: React.ReactNode;
};

function Layout({ children }: LayoutProps) {
return <div>{children}</div>;
}

类型React.ReactNode相对宽松,它几乎允许我们传递任何 react 能渲染的东西。

如果想更加严格的约束并且只能传递 jsx ,可以使用:

  • React.ReactElement
  • JSX.Element 这两种类型基本是相同的。并且相对于React.ReactNode更具限制性。

TS 项目中使用第三库​

添加类型​

如今,许多第三方库已经附带了相应的类型。在这种情况下,不需要安装单独的包。

但许多类型也维护在 GitHub 上的 DefinitelyTyped 存储库中,并在 @types 组织下发布(甚至 React 类型)。如果您安装没有类型的库,您会在导入语句中收到一条错误消息。

解决方式就是按照提示,复制粘贴对应的安装命令:

npm i --save-dev @types/styled-components

在大部分场景中,所需要的类型可以直接引用和使用,尤其是比较流行的第三方库。但是如果某些小众的且 ts 类型不那么完善的,我们可以在 .d.ts文件中定义自己的 全局类型

使用泛型​

npm 包里面通常需要处理各种的使用场景,因此开发人员需要更加灵活的类型。所以泛型经常在第三方库中使用和定义。就像前面说到的useState也是使用了泛型定义一样的道理。

const [names, setNames] = useState<string[]>([]);

以下是经常用到的第三方 npm 包: axios 案例

import axios from "axios"

async function fetchUser() {
const response = await axios.get<User>("https://example.com/api/user");
return response.data;
}

react-query案例

import { useQuery } from "@tanstack/react-query";

function UserProfile() {

// 给 data 和 error 分配的泛型
const { data, error } = useQuery<User, Error>(["user"], () => fetchUser());

if (error) {
return <div>Error: {error.message}</div>;
}
// ...
}

styled-components案例

import styled from "styled-components";

// generic type for props
const MenuItem = styled.li<{ isActive: boolean }>`
background: ${(props) => (props.isActive ? "red" : "gray")};
`;

function Menu() {
return (
<ul>
<MenuItem isActive>Menu Item 1</MenuItem>
</ul>
);
}

快速上手和排查问题​

开始一个 react + ts 项目​

使用 TypeScript 创建一个新项目是最简单的选择。此处示例创建一个 Vite + React + TypeScript 或 Next.js + TypeScript 项目。

# for Vite run this command and select "react-ts"
npm create vite@latest

# for Next.js run
npx create-next-app@latest --ts

找到正确的类型​

  • 事件类型可以通过先写一个内联的事件回调,鼠标悬停到 event 上,可以获取到对应的类型提示
  • 如果不知道有多少个传参的情况,可以使用同样的方法,先写一个内联的事件回调,(...args)=>{},鼠标悬停获取对应的类型。

检查一个类型​

查看类型上所有可用字段的最简单方法是使用 IDE 的自动完成功能。在此处按 CTRL + 空格键 (Windows) 或 Option + 空格键 (Mac)。

如果要查看更深的类型依赖关系, ctrl/cmd + 鼠标。

解读错误信息​

开始使用 TypeScript 时的一个主要问题是遇到的所有错误。典型的场景就是:我们拉去一个 ts 项目到本地,在没有依赖的时候打开全是报错,各种红色的波浪线。

下面示例一个简单的报错信息:

function Input() {
return <input />;
}

function Form() {

return (
<form>
<Input onChange={() => console.log("change")} />
</form>
);
}

ts 项目有个特点,几乎所有错误和不符合 ts 规范的都会在IDE以红色波浪线的形式标注出来。

什么是 IntrinsicAttributes 类型?在使用库(例如 React 本身)时,你会遇到许多像这样的奇怪类型名称。

建议:暂时忽略它们。最重要的信息在最后一行:类型上不存在属性“onChange”...

看上面这个案例:<Input />组件并没有一个叫做onChange的属性,这就是 ts 报错的原因。

现在我们再看一个非常基础的例子:

const MenuItem = styled.li`
background: "red";
`;

function Menu() {

return <MenuItem isActive>Menu Item</MenuItem>;
}

看看这夸张的大量报错信息,很容易造成困扰。最实在的方法是直接滚动到悬浮弹窗的最底部。最有价值的亮点就在这里。

将对象作为 props 可以让类型更整洁​

在上面的示例中,用户数据通常来自 API。假设我们在组件外部定义了 User 类型:

export type User = {
firstName: string;
role: UserRole;
}

此时的 UserProfile 组件正是将这个对象作为 props。

function UserProfile({ firstName, role }: User) { 

...

}

现在这似乎是合理的。当我们有一个现成的用户对象时,渲染这个组件实际上非常容易。

function UserPage() {
const user = useFetchUser();

return <UserProfile {...user} />;
}

但是一旦我们想要添加不包含在User类型中的额外 props,我们就会让我们犯难。还记得上面的 fireUser 函数吗?让我们开始使用吧。

function UserProfile({ firstName, role, fireUser }: User) {
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser({ firstName, role })}>
Fire this loser!
</button>
</>
);
}

但是由于 fireUser 函数没有在 User 类型上定义,我们遇到了一个报错。

要创建正确的类型,我们可以通过将两种类型与 & 组合来使用所谓的 交叉类型。(交叉类型是将多个类型合并为一个类型。)

type User = {
firstName: string;
role: UserRole;
}

type UserProfileProps = User & { // 交叉类型 UserProfileProps 拥有两种类型的所有字段
fireUser: (user: User) => void;
}

function UserProfile({ firstName, role, fireUser }: UserProfileProps) {
return (
<>
<div>Hi {firstName}, you suck!</div>
<button onClick={() => fireUser({ firstName, role })}>
Fire this loser!
</button>
</>
);
}

相对于 交叉类型 ,分离类型更简洁。在下面这个案例中,我们可以使用用户属性而不是直接接受所有用户字段。

type User = {
firstName: string;
role: UserRole;
}

type UserProfileProps = {
user: User;
fireUser: (user: User) => void;
}

function UserProfile({ user, onClick }: UserProfileProps) {

return (
<>
<div>Hi {user.firstName}, you suck!</div>
<button onClick={() => fireUser(user)}>
Fire this loser!
</button>
</>
);
}

原文: Minimal TypeScript Crash Course For React

react 新文档 推荐使用的 use-immer

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

之前遇到一个页面多个状态,绞尽脑汁思考怎么颗粒度最优,现在发现大意了。函数组件更新状态就会重新执行一次,以前是用多个状态管理的组件,现在可以实现一个对象就行了。

安装​

npm install immer use-immer

使用​

使用方式跟useState很相似,const [value, setValue] = useImmer(initialState)。

setValue接收一个回调函数或者一个值。 当传入回调时,形如:setValue(draft=>{ draft.name=name })draft 的值可以随意更改。

沙盒示例

封装hook​

正如上面提到的更新状态的方式: setValue(draft=>{ draft.name=name }) ,假如有多个状态分别控制不同的输入/选择组件(受控), 那么也许会在每个 onChange事件里面写:

const [pay, setPay] = useImmer({
name: '',
country: '',
note: '',
})

<TextField
value={pay.note}
onChange={(value) => setPay(draft=>{draft.note=value})}
/>

重用相似的逻辑,封装成hook​

useImmerHandler.ts
import { useCallback } from 'react'

export const useImmerHandler = (setState: React.Dispatch<any>) => {
const handler = useCallback(
(prop: string, value: any) => {
setState((draft: any) => {
draft[prop] = value
})
},
[setState]
)
return handler
}

使用hook​

const handlePaypal = useImmerHandler(setPay)

<TextField
value={pay.note}
onChange={(value) => handlePay('note', value)}
/>

ant 系列的 UI 引入 nprogress

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

由于之前一直在 shopify-polaris 的 UI 开发工作,antd、arco design 系列更适合中台业务,最近尝试用 arco 做了一个中台系统,其实这两个差不多,然后 arco 个人感觉更好看,没有其他理由。当然前段时间 ant 更新了 ant5 ,arco 和 antd 的实现差别就很大了。详情不多说了。

在 polaris 里面自带有个 loading 组件的,然后在 arco 里面没看到类似的组件, 发现他们用的 nprogress 这个库,github 有将近 30k 的 ⭐️,在此记录一下。

安装​

npm install --save nprogress

使用​

import 'nprogress/nprogress.css' // 全局引入

import NProgress from 'nprogress'

NProgress.start()
NProgress.done()

封装成 hook​

import NProgress from 'nprogress'

export const useLoading = () => {
const setLoading = (boolean) => {
if (boolean) {
NProgress.start()
} else {
NProgress.done()
}
}

return { setLoading }
}

使用 hook​

const { setLoading } = useLoading()

const req = async () => {
setLoading(true)
await new Promise((resolve) => {
setTimeout(() => {
resolve()
}, 4000)
})
setLoading(false)
}

js 的 Intl 相对时间格式化

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

JavaScript 中的 Intl.RelativeTimeFormat() API 可用于获取人类可读的日期差异。

  • 时间负差
const olderDate = new Date('2022-10-31')
const currentDate = new Date('2022-11-01')

const diff = olderDate - currentDate

const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
console.log(formatter.format(Math.round(diff / 86400000), 'day'))
// yesterday
  • numeric 参数
const formatter = new Intl.RelativeTimeFormat('en', {
numeric: 'always',
})

console.log(formatter.format(Math.round(diff / 86400000), 'day'))
// 1 day ago
  • 时间正差
const formatter = new Intl.RelativeTimeFormat('en', {
numeric: 'auto',
style: 'long',
localeMatcher: 'best fit',
})

console.log(formatter.format(Math.round(diff / 86400000), 'day'))
// tomorrow

原文: Human-readable date difference in JavaScript