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

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

相关推荐
u***u68516 分钟前
React环境
前端·react.js·前端框架
X***E46317 分钟前
前端数据分析应用
前端·数据挖掘·数据分析
4***149020 分钟前
React社区
前端·react.js·前端框架
LFly_ice22 分钟前
学习React-24-路由传参
前端·学习·react.js
Lhuu(重开版1 小时前
CSS:动效布局动画
前端·css
Q***K551 小时前
前端构建工具
前端
laocooon5238578861 小时前
创建了一个带悬停效果的“我的个人主页“按钮
前端
2013编程爱好者2 小时前
Vue工程结构分析
前端·javascript·vue.js·typescript·前端框架
小满zs3 小时前
Next.js第十一章(渲染基础概念)
前端
不羁的fang少年4 小时前
前端常见问题(vue,css,html,js等)
前端·javascript·css