【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 小时前
Vue 中 keep-alive 组件的原理与实践详解
前端·vue.js·spring
FeelTouch Labs2 小时前
Nginx核心架构设计
运维·前端·nginx
雪球工程师团队3 小时前
别再“苦力”写后台,Spec Coding “跑” 起来
前端·ai编程
m0_471199633 小时前
【场景】前端怎么解决离线收银、数据同步异常等场景问题
前端·javascript
Curvatureflight3 小时前
前端性能优化实战:从3秒到300ms的加载速度提升
前端·人工智能·性能优化
用户99045017780093 小时前
ruoyi集成dmn规则引擎
前端
袋鱼不重3 小时前
AI入门知识点:什么是 AIGC、多模态、RAG、Function Call、Agent、MCP?
前端·aigc·ai编程
NuLL3 小时前
空值检测工具函数-统一规范且允许自定义配置的空值检测方案
前端
栀秋6663 小时前
“无重复字符的最长子串”:从O(n²)哈希优化到滑动窗口封神,再到DP降维打击!
前端·javascript·算法
鹿鹿鹿鹿isNotDefined3 小时前
Antd5.x 在 Next.js14.x 项目中,初次渲染样式丢失
前端·react.js·next.js