【useMergeState】react开源组件常用——组件受控与非受控

受控与非受控概念

大白话来讲就是受父组件控制和不受父组件控制。

所以打造一个受控和非受控的组件就是考虑以下几点:

  1. 非受控:组件内部自行根据交互完成数据更新和视图更新
  2. 受控:组件内部数据由外部传入,展示外部数据,数据更新通知给外部

1、非受控版本案例

tsx 复制代码
import React, { useEffect, useRef, useState } from "react@18"
import { createRoot } from 'react-dom@18'

function Calendar() {
  const [value, setValue] = useState();

  function changeValue(date: Date) {
      setValue(date);
  } 

  return <div>
    {mergedValue?.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  return <>
    <Calendar/>
  </>
}

const app = document.getElementById('app');
const root = createRoot(app!)
root.render(<App />);
      

2、受控版本案例

tsx 复制代码
import React, { useEffect, useRef, useState } from "react@18"
import { createRoot } from 'react-dom@18'

interface CalendarProps{
  value?: Date; // 外部数据
  defaultValue?: Date; // 默认值
  onChange?: (date: Date) => void; // 数据改变事件
}

function Calendar(props: CalendarProps) {
  
  const {
    value: propsValue,
    defaultValue,
    onChange
  } = props;

   // 有外部值时,数据由外部初始化
  const [value, setValue] = useState(() => {
    if (propsValue !== undefined) {
      return propsValue;
    } else {
      return defaultValue;
    }
  });

  const isFirstRender = useRef(true);

  // 当外部值由 xx --》 undefined的时候,是由受控 --〉 非受控,因此将数据控制权交会内部
  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setValue(propsValue);
    }
    isFirstRender.current = false;
  }, [propsValue]);

  // 外部数据存在,展示外部值,不存在,展示内部值
  const mergedValue = propsValue === undefined ? value : propsValue;

  // 外部数据存在,同步更新给外部,不存在,更新内部值  
  function changeValue(date: Date) {
    if (propsValue === undefined) {
      setValue(date);
    }
    onChange?.(date);
  } 

  return <div>
    {mergedValue?.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  return <>
    <span>受控:</span>
    <Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
    console.log(date.toLocaleDateString());
    }}/>
    <span>非受控:</span>
    <Calendar/>
  </>
}

const app = document.getElementById('app');
const root = createRoot(app!)
root.render(<App />);
      

3、最终案例

tsx 复制代码
import React, { useEffect, useRef, useState } from "react@18"
import { createRoot } from 'react-dom@18'

interface CalendarProps{
  value?: Date;
  defaultValue?: Date;
  onChange?: (date: Date) => void;
}

function Calendar(props: CalendarProps) {
  
  const {
    value: propsValue,
    defaultValue,
    onChange
  } = props;

  const [value, setValue] = useState(() => {
    if (propsValue !== undefined) {
      return propsValue;
    } else {
      return defaultValue;
    }
  });

  const isFirstRender = useRef(true);

  useEffect(() => {
    if(propsValue === undefined && !isFirstRender.current) {
      setValue(propsValue);
    }
    isFirstRender.current = false;
  }, [propsValue]);

  const mergedValue = propsValue === undefined ? value : propsValue;

  function changeValue(date: Date) {
    if (propsValue === undefined) {
      setValue(date);
    }
    onChange?.(date);
  } 

  return <div>
    {mergedValue?.toLocaleDateString()}
    <div onClick={()=> {changeValue(new Date('2024-5-1'))}}>2023-5-1</div>
    <div onClick={()=> {changeValue(new Date('2024-5-2'))}}>2023-5-2</div>
    <div onClick={()=> {changeValue(new Date('2024-5-3'))}}>2023-5-3</div>
  </div>
}

function App() {
  return <>
    <span>受控:</span>
    <Calendar defaultValue={new Date('2024-5-1')} onChange={(date) => {
    console.log(date.toLocaleDateString());
    }}/>
    <span>非受控:</span>
    <Calendar/>
  </>
}

const app = document.getElementById('app');
const root = createRoot(app!)
root.render(<App />);
      

Hook封装

Hook将重复部分提取,change方法也是重复,它也是一个方法,所以通过装饰来扩展它的能力,变为setState。

ts 复制代码
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"

export function useMergeState<T>(
    defaultStateValue: T, // 内部兜底默认值
    props?: {
        defaultValue?: T, // 外部默认值
        value?: T // 外部value
        onChange?: (value: T) => void;
    }
): [T, React.Dispatch<React.SetStateAction<T>>] {
    const { defaultValue, value: propsValue, onChange } = props || {}

    const isFirstRender = useRef(true)

    const [stateValue, setStateValue] = useState<T>(() => {
        if (propsValue !== undefined) {
            return propsValue
        } else if (defaultValue !== undefined) {
            return defaultValue
        } else {
            return defaultStateValue
        }
    })

    useEffect(() => {
        if (propsValue === undefined && !isFirstRender.current) {
            setStateValue(propsValue!)
        }
        isFirstRender.current = false
    }, [propsValue])

    const mergedValue = propsValue === undefined ? stateValue : propsValue

    /**
     * 装饰setState方法来扩展它的能力
    */
    function isFunction(value: unknown): value is Function {
        return typeof value === 'function'
    }

    const setState = useCallback((value: SetStateAction<T>) => {
        let res = isFunction(value) ? value(stateValue) : value

        if (propsValue === undefined) {
            setStateValue(res)
        }

        onChange?.(res)
    }, [stateValue])

    return [mergedValue, setState]
}

参考文章: # 深入理解受控组件、非受控组件

相关推荐
秋田君2 分钟前
Vue3 + VitePress 搭建部署组件库文档平台(结合 Element Plus 与 Arco Design Vue)—— 超详细图文教程
前端·vue.js·arco design
code_Bo3 分钟前
前端使用snapdom报错问题
前端·javascript·vue.js
一壶浊酒..23 分钟前
什么是AJAX
前端·javascript·ajax
fruge3651 小时前
从零到一:我在 Rokid Glasses 上“画”出一个远程协作系统
前端
BumBle1 小时前
高频扫码场景下的去重与接口调用方案
前端·javascript
Mapmost1 小时前
半透明模型渲染全白?Mapmost Studio一招搞定
前端
SpiderPex1 小时前
JavaWeb登录模块完整实现解析:从前端点击到后端验证的全流程
前端
可乐爱宅着1 小时前
在VSCode的next.js项目中更好的使用eslint/prettier工具
前端·next.js
_大学牲1 小时前
🫡我在掘金写文章:一气之下开源 视频转无水印GIF 插件
前端·javascript
地方地方1 小时前
深入理解 instanceof 操作符:从原理到手动实现
前端·javascript