React中的设计模式 - 受控属性Control Props

前言

最近开始学习React,跟着Kent学,有很多干货,这里分享Rect中的一个设计模式Control Props,这个设计方法跟React中的Control Component

文章中完整的示例代码可以查看 这里


一、background

1.1 需求

有时候用户希望能够控制state的改变,比如在Inpt的组件中,用户希望控制input中的value以及value改变的action,onChange;对于DOM组件来说,React已经提供了受控组件

对于自定义组件,也可以设计出受控props,只要用户需要,props的控制权应该可以交给用户

1.2 example

比如,对于自定义的Toggle组件,用户可以控制on以及onChange;如果用户不愿意控制,那么就使用props的default on以及onChange

 <Toggle on={bothOn} onChange={handleToggleChange} />
 <Toggle on={bothOn} onChange={handleToggleChange} />

二、实现

2.1 example

这里会借用之前的Toggle组件的例子,向外提供control props的功能

2.2 useToggle实现

useToggle主要是给组件调用的props的,比如用户新建Toggle组件,那么组件需要用的props统一从useToggle中获得

首先不妨先设计useToggle可以接受的参数,其中on以及inChange都是需要做成受控props的

function useToggle({
  initialOn = false,
  reducer = toggleReducer,
  onChange,
  on: controlledOn
} = {}) {

由于Toggle向外会提供两个方法,toggle以及reset,在user么有修改情况之下,使用dispatch方法就行了

const toggle = () => dispatch({type: actionTypes.toggle})
const reset = () => dispatch({type: actionTypes.reset, initialState})

但是如果需要提供受控props,那么如果user提供的话,就应该使用user提供的
这里不妨设想user控制props情景如下,user控制on,提供了onChange的方法

 <Toggle on={bothOn} onChange={handleToggleChange} />
 
 function handleToggleChange(state, action) {
   if (action.type === actionTypes.toggle && timesClicked > 4) {
     return
   }
   setBothOn(state.on)
   setTimesClicked(c => c + 1)
 }

也就是说,对于useToggle的dispatch来说需要处理两种情况

  1. user没有控制props,使用dispatch
  2. user控制props,调用user控制的方法

2.3 设计新的dispatch

根据上面的要求,不妨设计新的dispatch

 function dispatchWithOnChange(action) {
   if (!onIsControlled) {
     dispatch(action)
   }
   onChange?.(reducer({...state, on}, action), action)
 }
 
 const toggle = () => dispatchWithOnChange({type: actionTypes.toggle})
 const reset = () => dispatchWithOnChange({type: actionTypes.reset, initialState})

2.4 调用user的onChange逻辑

这里主要讲一下,调用user方法的逻辑

onChange?.(reducer({...state, on}, action), action)

从2.2的API可以知道,user调用的是

onChange = {()=>handleToggleChange(state, action)}

所以需要传新的state以及action,使用reduedr(state, action)可以返回新的state,由于on也是受控props,所以需要单独提取出来,如下

(reducer({...state, on}, action)

2.5 final version

全部的实现(Switch组件以及css)可以到前言中codesandbox链接中参考

import * as React from "react";
import { Switch } from "./switch";

const callAll = (...fns) => (...args) => fns.forEach((fn) => fn?.(...args));

const actionTypes = {
  toggle: "toggle",
  reset: "reset"
};

function toggleReducer(state, { type, initialState }) {
  switch (type) {
    case actionTypes.toggle: {
      return { on: !state.on };
    }
    case actionTypes.reset: {
      return initialState;
    }
    default: {
      throw new Error(`Unsupported type: ${type}`);
    }
  }
}

function useToggle({
  initialOn = false,
  reducer = toggleReducer,
  onChange,
  on: controlledOn
} = {}) {
  const { current: initialState } = React.useRef({ on: initialOn });
  const [state, dispatch] = React.useReducer(reducer, initialState);
  console.log("test on is controller ", controlledOn);
  const onIsControlled = controlledOn != null;
  const on = onIsControlled ? controlledOn : state.on;

  function dispatchWithOnChange(action) {
    if (!onIsControlled) dispatch(action);
    onChange?.(reducer({ ...state, on }, action), action);
  }

  const toggle = () => dispatchWithOnChange({ type: actionTypes.toggle });
  const reset = () =>
    dispatchWithOnChange({ type: actionTypes.reset, initialState });

  function getTogglerProps({ onClick, ...props } = {}) {
    return {
      "aria-pressed": on,
      onClick: callAll(onClick, toggle),
      ...props
    };
  }

  function getResetterProps({ onClick, ...props } = {}) {
    return {
      onClick: callAll(onClick, reset),
      ...props
    };
  }

  return {
    on,
    reset,
    toggle,
    getTogglerProps,
    getResetterProps
  };
}

function Toggle({ on: controlledOn, onChange }) {
  const { on, getTogglerProps } = useToggle({ on: controlledOn, onChange });
  const props = getTogglerProps({ on });
  return <Switch {...props} />;
}

function App() {
  const [bothOn, setBothOn] = React.useState(false);
  const [timesClicked, setTimesClicked] = React.useState(0);

  function handleToggleChange(state, action) {
    if (action.type === actionTypes.toggle && timesClicked > 4) {
      return;
    }
    setBothOn(state.on);
    setTimesClicked((c) => c + 1);
  }

  function handleResetClick() {
    setBothOn(false);
    setTimesClicked(0);
  }

  return (
    <div>
      <div>
        <Toggle on={bothOn} onChange={handleToggleChange} />
        <Toggle on={bothOn} onChange={handleToggleChange} />
      </div>
      {timesClicked > 4 ? (
        <div data-testid="notice">
          Whoa, you clicked too much!
          <br />
        </div>
      ) : (
        <div data-testid="click-count">Click count: {timesClicked}</div>
      )}
      <button onClick={handleResetClick}>Reset</button>
      <hr />
      <div>
        <div>Uncontrolled Toggle:</div>
        <Toggle
          onChange={(...args) =>
            console.info("Uncontrolled Toggle onChange", ...args)
          }
        />
      </div>
    </div>
  );
}

export default App;
export { Toggle };


总结

受控props可以类比受控组件,目的就是为了让用户有更多的控制权,以方便用户更多自定义的方法,提高了组建的可复用性

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值