WangShuXian6 / blog

FE-BLOG
https://wangshuxian6.github.io/blog/
MIT License
46 stars 10 forks source link

my react hooks #96

Open WangShuXian6 opened 4 years ago

WangShuXian6 commented 4 years ago

hooks

WangShuXian6 commented 4 years ago

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>
  );
};
WangShuXian6 commented 4 years ago

可重用的 React Hooks表单处理程序

表单具有两种主要类型的事件处理程序:

onSubmit –处理表单提交。

onChange –处理更改任何表单输入值。

每个表单都有这些事件处理程序

因此,可以重用 onSubmitonChange

核心代码 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;
WangShuXian6 commented 4 years ago

可重用的 带表单验证的 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;
WangShuXian6 commented 4 years ago

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>
    )
}
WangShuXian6 commented 3 years ago

如何定义“组件”

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 (

我是子组件

子组件的number是{value}

);

}, [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( () => (

我是父组件

父组件的count是{countRef.current}

),
[B]

); return A; }

function App() { const A = useServiceA(); return useMemo(() => , [A]); }

export default App;

WangShuXian6 commented 3 years ago

分页查询 自定义响应标识【应使用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(false); const [error, setError] = useState(null); const [newsList, setNewsList] = useState<API.News[]>([]); const [total, setTotal] = useState(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;

WangShuXian6 commented 3 years ago

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

WangShuXian6 commented 4 months ago

useQuery 请求hooks

通用请求工具

src\useQuery.ts

import { useState, useEffect } from "react";

export interface QueryResult<T,P> {
  data: T | null;
  error: Error | null;
  isLoading: boolean;
  exec: (p: P) => void;
}

// 通用请求 hooks
const useQuery = <T, P>(
  queryFn: (params: P) => Promise<T>,
  params: P
): QueryResult<T,P> => {
  const [data, setData] = useState<T | null>(null);
  const [error, setError] = useState<Error | null>(null);
  const [isLoading, setIsLoading] = useState<boolean>(false);

  const exec = async (newParams: P) => {
    setIsLoading(true);
    try {
      const response = await queryFn(newParams || params);
      setData(response);
    } catch (error) {
      setError(error as Error);
    } finally {
      setIsLoading(false);
    }
  };

  useEffect(() => {
    exec(params);
  }, []);

  return { data, error, isLoading, exec };
};

export default useQuery;

使用通用请求工具hooks 封装业务请求hooks useTestQuery

src\useTestQuery.ts


import useQuery, { QueryResult } from "./useQuery";

type Params = { a: number; };

type Response = { b: number; };

// 模拟请求 const getData = (params: Params): Promise => { return new Promise((resolve, reject) => { setTimeout(() => { resolve({ b: params.a * 2 }); }, 5000); }); };

// 使用请求的hooks服务 const useTestQuery = (params: Params): QueryResult<Response, Params> => { return useQuery<Response, Params>(getData, params); };

export default useTestQuery;


>`src\App.tsx`
```tsx
import { useEffect, useState } from "react";
import useTestQuery from "./useTestQuery";
import "./App.css";

function App() {
  const { data, error, isLoading, exec } = useTestQuery({ a: 10 });

  useEffect(() => {
    setTimeout(() => {
      exec({ a: 20 });
    }, 10000);
  }, []);
  return (
    <>
      {isLoading && <div>加载中...</div>}
      {error && error.message}
      {!isLoading && data && data?.b}
    </>
  );
}

export default App;

加载,10秒后显示20,20秒后显示40.

WangShuXian6 commented 4 months ago

hooks 类型

泛型属性

interface Props<T> {
  list: T;
}

interface Return {}

export const useSwipeHandler = <T>({ list }: Props<T>): Return => {
  return {};
};
WangShuXian6 commented 1 week ago

获取并监视css 变量的值

在 TypeScript 中,可以为这个自定义 Hook 添加类型定义,以确保传入的变量名称是一个字符串,并且返回的变量值也是一个字符串。以下是带有 TypeScript 类型的实现:

import { useState, useEffect } from 'react';

function useCSSVariable(variableName: string): string {
  const [variableValue, setVariableValue] = useState<string>('');

  useEffect(() => {
    const root = document.documentElement;

    // 获取 CSS 变量的当前值
    const getCSSVariableValue = (): string => {
      return getComputedStyle(root).getPropertyValue(variableName).trim();
    };

    // 初始化变量值
    setVariableValue(getCSSVariableValue());

    // 创建 MutationObserver 实例来监视变量变化
    const observer = new MutationObserver(() => {
      setVariableValue(getCSSVariableValue());
    });

    // 监视根元素的属性变化
    observer.observe(root, { attributes: true, attributeFilter: ['style'] });

    // 清理观察者
    return () => observer.disconnect();
  }, [variableName]);

  return variableValue;
}

export default useCSSVariable;

使用示例

与之前相同,只是在 TypeScript 文件(.tsx)中使用:

import React from 'react';
import useCSSVariable from './useCSSVariable';

const App: React.FC = () => {
  const mainColor = useCSSVariable('--main-color');

  return (
    <div>
      <p>The main color is: {mainColor}</p>
    </div>
  );
};

export default App;

功能解释

  • variableName: string:接受一个字符串参数作为 CSS 变量名称。
  • 返回值类型 string:表示返回的 CSS 变量的当前值是一个字符串。