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

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

相关推荐
阿珊和她的猫3 分钟前
动态指令参数:根据组件状态调整指令行为
前端·javascript·vue.js
xiegwei5 分钟前
vue+element 导航 实现例子
前端·javascript·vue.js
露临霜1 小时前
vue实现AI问答Markdown打字机效果
前端·javascript·vue.js·ai·github
愛芳芳2 小时前
springboot+mysql+element-plus+vue完整实现汽车租赁系统
前端·vue.js·spring boot·后端·mysql·elementui·汽车
m0_zj3 小时前
55.[前端开发-前端工程化]Day02-包管理工具npm等
前端·npm·node.js
xcLeigh5 小时前
HTML5好看的水果蔬菜在线商城网站源码系列模板9
java·前端·html5·网页源码
星仔_X5 小时前
硬件加速模式Chrome(Edge)闪屏
前端·chrome·edge
2501_915373885 小时前
使用 Vue + Axios 构建与后端交互的高效接口调用方案
前端·vue.js·交互
莫问alicia8 小时前
react + antd 实现后台管理系统
前端·react.js·前端框架·antd
七灵微10 小时前
ES6入门---第三单元 模块三:async、await
前端·javascript·es6