【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]
}

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

相关推荐
MM_MS4 分钟前
Halcon控制语句
java·大数据·前端·数据库·人工智能·算法·视觉检测
程序员Agions14 分钟前
程序员武学修炼手册(二):进阶篇——小有所成,从能跑就行到知其所以然
前端·程序员
小画家~18 分钟前
第四十六: channel 高级使用
java·前端·数据库
小贵子的博客28 分钟前
Ant Design Vue <a-table>
前端·javascript·vue.js·anti-design-vue
m0_5027249530 分钟前
vue动态设置背景图片后显示异常
前端·css
console.log('npc')37 分钟前
vue2中子组件父组件的修改参数
开发语言·前端·javascript
奋斗吧程序媛1 小时前
vue3 Study(1)
前端·javascript·vue.js
@Autowire1 小时前
Layout-position
前端·css
QQ12958455041 小时前
ThingsBoard - APP首页修改为手工选择组织
前端·javascript·物联网·iot
whyfail1 小时前
前端数据存储新选择:IndexedDB与Dexie.js技术指南
前端·javascript·数据库