blog
blog copied to clipboard
my react hooks
hooks
useDom
获取 hook 所在元素节点
useDom
import React, {
Component,
useEffect,
useLayoutEffect,
useReducer,
useState,
useContext,
useRef,
useCallback,
useMemo
} from "react";
function useDom(): [{ ref: any }] {
const ref = useRef();
const bind = useMemo(() => {
return {
ref
};
}, []);
useEffect(() => {
console.log("ref dom:", ref.current);
}, [ref]);
return [bind];
}
export default useDom;
useDom 使用示例
import useDom from '../useDom'
const DomExample = () => {
const [bindText] = useDom()
return (
<Text className='tip-bar' {...bindText}>现在开始</Text>
);
};
useTouch
touch 事件
useTouch
import React from 'react';
function useTouch(): [
boolean,
{
onTouchStart: (e: React.TouchEvent) => void;
onTouchEnd: (e: React.TouchEvent) => void;
}
] {
const [isTouched, setTouched] = React.useState(false);
const bind = React.useMemo(
() => ({
onTouchStart: (e: React.TouchEvent) => void setTouched(true),
onTouchEnd: (e: React.TouchEvent) => void setTouched(false),
}),
[]
);
return [isTouched, bind];
}
export default useTouch;
useTouch 使用示例
const Example = () => {
const [isTouched, bind] = useTouch();
return (
<div {...bind}>
{isTouched ? '触摸到' : '未触摸到'}
</div>
);
};
可重用的 React Hooks表单处理程序
表单具有两种主要类型的事件处理程序:
onSubmit
–处理表单提交。
onChange
–处理更改任何表单输入值。
每个表单都有这些事件处理程序
因此,可以重用
onSubmit
和onChange
核心代码 useForm.js
自定义React Hooks使用特殊的命名约定,即放置“ use”在前 函数名称的名称,以便React知道该文件是一个Hook
useForm.ts
import { useState } from 'react';
const useForm = (initialValues: object, callback: Function) => {
// 该函数带有2个参数initialValues,callback。
// initialValues 是表单可能拥有的输入值组成的 name:value 对象
// callback 回调是从组件传递到自定义挂钩的函数。表单提交时都会调用它。
// 设置一个状态变量和一个setter函数,分别称为values和setValues,跟踪表单值
const [values, setValues] = useState<object>(initialValues);
// 该函数接受一个事件。它可以防止该事件的默认操作(在事件被调用后刷新页面)。之后,它仅调用callback()
const handleSubmit = (event) => {
if (event) event.preventDefault();
callback();
};
// 该函数也接收一个事件
const handleChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
// 返回handleChange,handleSubmit和值,以便我们的组件可以访问它们
return {
handleChange,
handleSubmit,
values,
}
};
export default useForm;
使用 useForm 创建有状态组件 表单
Form.jsx
import React from 'react';
import useForm from "./useForm";
const Form = () => {
//分解从useForm自定义React Hook返回的对象,以便我们可以使用值,handleChange和handleSubmit
const { values, handleChange, handleSubmit } = useForm({ email: '', password: '' }, login); // 初始化
// 向Form组件添加一个登录函数,并将其作为回调参数传递到useForm自定义Hook中
function login() {
console.log(values);
}
return (
// 将onSubmit属性添加到表单HTML元素,然后调用handleSubmit:
<form onSubmit={handleSubmit}>
<div className="field">
<label className="label">Email Address</label>
<div className="control">
{/* {电子邮件输入,向其添加onChange和value属性} */}
<input className="input" type="email" name="email" onChange={handleChange} value={values.email} required />
</div>
</div>
<div className="field">
<label className="label">Password</label>
<div className="control">
{/* {密码输入,向其添加onChange和value属性} */}
<input className="input" type="password" name="password" onChange={handleChange} value={values.password} required />
</div>
</div>
<button type="submit" className="button is-block is-info is-fullwidth">Login</button>
</form>
);
};
export default Form;
可重用的 带表单验证的 React Hooks 表单处理程序
为了验证表单,我们需要做几件事:
定义表单的验证规则
将任何错误存储在状态变量中
如果存在任何错误,阻止表单提交
定义验证规则
LoginFormValidationRules.ts
export default function validate(values:object) {
// 该函数接受一个参数 name:value 格式的对象
//初始化一个称为errors的新对象
let errors = {};
// 为电子邮件输入字段添加一个验证规则
if (!values.email) {
errors.email = 'Email address is required';
} else if (!/\S+@\S+\.\S+/.test(values.email)) {
errors.email = 'Email address is invalid';
}
if (!values.password) {
errors.password = 'Password is required';
} else if (values.password.length < 8) {
errors.password = 'Password must be 8 or more characters';
}
// 返回错误对象,以便我们枚举useForm自定义挂钩中的错误
return errors;
};
核心代码 useForm.ts
useForm.ts
import { useState, useEffect } from 'react';
const useForm = (initialValues: object, callback: () => void, validate: (object) => object) => {
// 该函数带有3个参数initialValues,callback。
// initialValues 是表单可能拥有的输入值组成的 name:value 对象
// callback 回调是从组件传递到自定义挂钩的函数。表单提交时都会调用它。
// validate 验证函数 返回错误对象
// 设置一个状态变量和一个setter函数,分别称为values和setValues,跟踪表单值
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState({});
// 防止表单在渲染时提交
const [isSubmitting, setIsSubmitting] = useState(false);
// useEffect替换了React Class组件中的componentDidMount和componentDidUpdate生命周期方法
// 侦听对错误的任何更改,检查对象的长度,如果errors对象为空,则调用回调函数
// [errors]更改的结果(副作用),请执行此操作
// 组件渲染时被调用,如果不检测 isSubmitting ,将在页面初始化的时候提交表单 执行 callback()
useEffect(() => {
if (Object.keys(errors).length === 0 && isSubmitting) {
callback();
}
}, [errors]);
// 该函数接受一个事件。它可以防止该事件的默认操作(在事件被调用后刷新页面)。之后,它仅调用callback()
const handleSubmit = (event) => {
if (event) event.preventDefault();
// 当用户提交表单时,我们首先要在提交表单之前检查其所有数据是否没有问题
setErrors(validate(values));
setIsSubmitting(true);
};
const handleChange = (event) => {
event.persist();
setValues(values => ({ ...values, [event.target.name]: event.target.value }));
};
// 返回handleChange,handleSubmit和值,错误对象,以便我们的组件可以访问它们
return {
handleChange,
handleSubmit,
values,
errors,
}
};
export default useForm;
使用 useForm 创建有状态组件 表单
Form.jsx
import React from 'react';
import useForm from "./useForm";
import validate from './LoginFormValidationRules';
const Form = () => {
//分解从useForm自定义React Hook返回的对象,以便我们可以使用值,handleChange和handleSubmit
const {
values,
errors,
handleChange,
handleSubmit,
} = useForm(login, validate);
// 向Form组件添加一个登录函数,并将其作为回调参数传递到useForm自定义Hook中
function login() {
console.log('No errors, submit callback called!');
}
return (
// 将onSubmit属性添加到表单HTML元素,然后调用handleSubmit:
<form onSubmit={handleSubmit} noValidate>
<div className="field">
<label className="label">Email Address</label>
<div className="control">
{/* {电子邮件输入,向其添加onChange和value属性} */}
{/* {通过检查errors对象是否具有与输入名称匹配的键来使用该类,如果是,则将 is-danger 类添加到输入的className中} */}
<input autoComplete="off" className={`input ${errors.email && 'is-danger'}`} type="email" name="email" onChange={handleChange} value={values.email || ''} required />
{/* 通过在输入元素下方添加内联条件来显示实际错误消息,以再次检查errors对象是否具有与该输入匹配的键,如果是,则以红色显示错误消息: */}
{errors.email && (
<p className="help is-danger">{errors.email}</p>
)}
</div>
</div>
<div className="field">
<label className="label">Password</label>
<div className="control">
{/* {密码输入,向其添加onChange和value属性} */}
<input className={`input ${errors.password && 'is-danger'}`} type="password" name="password" onChange={handleChange} value={values.password || ''} required />
</div>
{errors.password && (
<p className="help is-danger">{errors.password}</p>
)}
</div>
<button type="submit" className="button is-block is-info is-fullwidth">Login</button>
</form>
);
};
export default Form;
taro 专属的 可重用的 弹窗遮罩 useTaroMask
有状态遮罩层组件
TaroMask.tsx
import React, { useEffect, useState } from 'react'
import { View } from "@tarojs/components";
import "./TaroMask.less";
// dts
// enum Direction {
// 'center' = 'center',
// 'top' = 'top',
// 'bottom' = 'bottom'
// }
// util
// type Definition
type OwnProps = {
isDisableMaskEvent?: boolean; //是否禁止背景点击关闭
direction?: 'center' | 'top';
visible: boolean;
onToggle?: () => void;
children: any;
};
export default function TaroMask({ isDisableMaskEvent, direction, visible, onToggle, children }: OwnProps) {
const [directionClassName, setDirectionClassName] = useState<string>('')
const [visibleClassName, setVisibleClassName] = useState<string>('taro-mask_container')
useEffect(() => {
const className = visible ? "taro-mask_container show" : "taro-mask_container"
setVisibleClassName(className)
}, [visible])
useEffect(() => {
console.log('direction:', direction)
if (direction === 'top') {
setDirectionClassName('top')
return
}
// if (direction === 'bottom') {
// setDirectionClassName('bottom')
// return
// }
setDirectionClassName('center')
}, [direction])
const toggle = () => {
if (isDisableMaskEvent) return
if (!onToggle) return
onToggle()
}
const stopEvent = (e) => {
e.preventDefault()
e.stopImmediatePropagation()
e.stopPropagation()
}
return (
<View className={`${visibleClassName} ${directionClassName}`} onClick={toggle}>
<View onClick={stopEvent}>
{children}
</View>
</View>
);
}
TaroMask.less
.taro-mask_container {
z-index: -1;
opacity: 0;
visibility: hidden;
position: fixed;
top: 0;
left: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
width: 100vw;
height: 100vh;
overflow-x: hidden;
overflow-y: auto;
transition: all ease-in-out 0.2s;
background-color: rgba(0, 0, 0, 0.2);
}
.taro-mask_container.top {
position: fixed;
top: 0;
left: 0;
align-items: flex-start;
}
// .taro-mask_container.bottom {
// position: fixed;
// top: 0;
// left: 0;
// align-items: flex-end;
// }
.taro-mask_container.show {
position: fixed;
top: 0;
left: 0;
z-index: 100;
opacity: 1;
visibility: visible;
transition: all ease-in-out 0.2s;
}
核心代码 useTaroMask
useTaroMask.ts
import React, { useState, useRef } from "react";
import TaroMask from "./TaroMask";
export default TaroMask;
// dts
// enum Direction {
// "center" = "center",
// "top" = "top",
// "bottom" = "bottom"
// }
interface Params {
isDisableMaskEvent?: boolean; //是否禁止背景点击关闭
direction?: "center" | "top";
visible: boolean;
}
export const useTaroMask = (
params: Params
): [
boolean,
() => void,
{
onToggle: () => void;
visible: boolean;
direction: "center" | "top";
isDisableMaskEvent?: boolean;
}
] => {
const [visible, setVisible] = useState<boolean>(params.visible);
const countRef = useRef(params.visible);
countRef.current = visible;
const toggle = () => {
//if (params.isDisableMaskEvent && visible) return;
setVisible(!countRef.current);
};
return [
visible,
toggle,
{
onToggle: toggle,
visible,
direction: params.direction || "center",
isDisableMaskEvent: params.isDisableMaskEvent
}
];
};
使用示例
example.tsx
//hooks
import TaroMask, { useTaroMask } from '../../hooks/taroMask/useTaroMask'
export default function Index({ }: OwnProps) {
const [tipVisible, toggleTip, tipBind] = useTaroMask({ visible: true }) // 首屏弹窗
useEffect(() => {
setTimeout(() => {
toggleTip()
}, 10000)
}, [1])
return (
<TaroMask {...tipBind}>
{/* 自定义内容区域 */}
</TaroMask>
)
}
如何定义“组件”
UI 拆分为独立可复用的代码片段
别纠结它渲染多少次了,因为你的组件只是在拆分 UI,所以 UI 一致就行,逻辑一不一致不在你考虑范围内
应用程序的不同功能单元(称为服务)
视图 该如何处理?在下面的服务中,视图被视为 VM 适配器 如果你需要 props -> 处理 -> VM 的模型,那么你实际上干了分层的事情
import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react';
import logo from './logo.svg';
import './App.css';
function useServiceB(count: number) {
const [value, setValue] = useState(count);
useEffect(() => {
setValue(count);
}, [count]);
const B = useCallback(() => {
console.log("子组件render");
return (
<div>
<p>我是子组件</p>
<p>子组件的number是{value}</p>
<button onClick={() => setValue((res) => ++res)}>click</button>
</div>
);
}, [value]);
return B;
}
function useServiceA() {
const [count, setCount] = useState(0);
const B = useServiceB(count);
// 欢乐消依赖
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const A = useCallback(
() => (
<div>
<p>我是父组件</p>
<p>父组件的count是{countRef.current}</p>
<button onClick={() => setCount(count + 1)}>click</button>
<B />
</div>
),
[B]
);
return A;
}
function App() {
const A = useServiceA();
return useMemo(() => <A />, [A]);
}
export default App;
分页查询 自定义响应标识【应使用HTTP标识】
import { useEffect, useState } from 'react';
import { queryNewsList } from '@/services/management/news';
interface Params {
lazy?: boolean;
page?: number;
pageSize?: number;
}
interface Return {
loading: boolean;
newsList: API.News[];
total: number;
error: any;
refetch: () => void;
}
const useNewsList = ({ lazy = false, page = 1, pageSize = 10 }: Params): Return => {
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<any>(null);
const [newsList, setNewsList] = useState<API.News[]>([]);
const [total, setTotal] = useState<number>(0);
const query = async () => {
setLoading(true);
try {
const res = await queryNewsList({ page, pageSize });
const { status, reason, result } = res || {};
const { message } = reason || {};
const { list: newsList, total } = result || {};
const isSuccess = status === 0;
if (!isSuccess || !newsList) throw new Error(message);
setNewsList(newsList);
setTotal(total);
} catch (error) {
setError(error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (lazy) return;
query();
}, []);
return Object.assign([loading, newsList, total, error, query], {
loading,
newsList,
total,
error,
refetch: query,
});
};
export default useNewsList;
useRequest
js
common.js
export const removeEmpty = (obj) => {
Object.keys(obj).forEach(
(key) =>
(obj[key] == null || obj[key] == undefined || obj[key] == "") &&
delete obj[key]
);
return obj;
};
request.js
import { removeEmpty } from "./common";
import { trackPromise} from 'react-promise-tracker';
const request = (url, { method, payload, headers }) => {
return new Promise((resolve, reject) => {
const timeHeader = {
"Content-Type": "application/json",
};
const headersWithTime = removeEmpty(
Object.assign({}, headers || {}, timeHeader)
);
const _payload = removeEmpty(payload);
trackPromise(fetch(url, {
method: method || "GET",
body: method === "GET" ? null : JSON.stringify(_payload || {}),
headers: headersWithTime,
})
.then((res) => {
res.text().then((responseText) => {
const response = JSON.parse(responseText);
return resolve(response);
});
})
.catch((error) => {
console.warn("error", error);
return reject(error);
}))
});
};
export default request;
useRequest.js
import { useState, useEffect } from "react";
import request from "../utils/request";
const defaultFn = () => {};
const useRequest = (api, params = {}, callBack = {}, lazy) => {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(false);
const _request = async (api, _params = {}, _callBack = {}) => {
const method = _params.method || params.method || "GET";
const query = _params.query || params.query || {};
const payload = _params.payload || params.payload || {};
const headers = _params.headers || params.headers || {};
const onCompleted =
_callBack.onCompleted || callBack.onCompleted || defaultFn;
const onSuccess = _callBack.onSuccess || callBack.onSuccess || defaultFn;
const onFail = _callBack.onFail || callBack.onFail || defaultFn;
setLoading(false);
const loadingIntervel = setInterval(() => {
setLoading(true);
}, 200);
try {
const params = {
method,
payload,
headers,
};
const res = await request(api, params);
const { status, reason, result = {} } = res || {};
const { message } = reason || {};
const isSuccess = status === 1;
if (!isSuccess) throw new Error(message);
setData(result);
onCompleted && onCompleted(result);
onSuccess && onSuccess(result);
return {
data: result,
success: true,
};
} catch (error) {
setError(error);
setData(null);
onFail && onFail(error);
} finally {
clearInterval(loadingIntervel);
setLoading(false);
onCompleted && onCompleted(null);
}
};
const refetch = (_params, _callBack) => {
_request(api, _params, _callBack);
};
useEffect(() => {
if (lazy) return;
refetch(
{
query: params.query || {}, //暂不支持
payload: params.payload || {},
},
{
onCompleted: callBack.onCompleted || defaultFn,
onSuccess: callBack.onSuccess || defaultFn,
onFail: callBack.onFail || defaultFn,
}
);
}, []);
return Object.assign([data, loading, error, refetch], {
data,
loading,
error,
refetch,
});
};
export default useRequest;
示例
useJob
import useRequest from "../hooks/useRequest";
import { jobApi } from "../config/api";
const useJob = ({ empId, lazy }={}) => {
const { data, loading, error, refetch } = useRequest(
jobApi,
{
method: "POST",
payload: { empId },
},
{},
lazy
);
const _refetch = (params) => {
refetch();
};
return Object.assign([data, loading, error, _refetch], {
job: data,
loading,
error,
refetch: _refetch,
});
};
export default useJob;
import { useJob } from "../../hooks";
const { loading, job } = useJob({ empId: id });