如何在React中用Render Props实现类似Tailwind Switch的状态暴露
如何实现带Render Props的Switch组件并暴露内部状态
核心逻辑:Render Props的本质
Render Props模式的核心是让组件的children接收一个函数,组件内部将自身的状态、回调等数据作为参数传给这个函数,从而实现内部状态向外部的暴露。
第一步:从零实现基础版Switch
先写一个最简单的Switch,内部维护状态并通过Render Props暴露:
import { useState } from 'react'; function BasicSwitch({ children }) { // 内部维护checked状态 const [checked, setChecked] = useState(false); // 状态切换回调 const handleToggle = () => { setChecked(!checked); }; // 调用children函数,传入需要暴露的状态和回调 return children({ checked, onChange: handleToggle }); } // 使用方式 function MyToggle() { return ( <BasicSwitch> {({ checked, onChange }) => ( <button onClick={onChange} className={`${checked ? 'bg-blue-600' : 'bg-gray-200'} relative inline-flex h-6 w-11 items-center rounded-full`} > <span className="sr-only">切换状态</span> <span className={`${checked ? 'translate-x-6' : 'translate-x-1'} inline-block h-4 w-4 transform rounded-full bg-white transition`} /> </button> )} </BasicSwitch> ); }
第二步:支持受控/非受控双模式
Tailwind的Switch同时支持外部控制状态(受控模式)和内部维护状态(非受控模式),这是通过useControllable钩子实现的。我们可以自己实现一个简易版:
function useControllable(controlledValue, onChange, defaultValue) { // 非受控模式下的内部状态 const [internalValue, setInternalValue] = useState(defaultValue); // 判断是否为受控模式:外部传入了controlledValue const isControlled = controlledValue !== undefined; // 当前值:受控用外部值,非受控用内部值 const currentValue = isControlled ? controlledValue : internalValue; // 统一的变更处理函数 const handleChange = (newValue) => { // 优先调用外部传入的onChange onChange?.(newValue); // 非受控模式下更新内部状态 if (!isControlled) { setInternalValue(newValue); } }; return [currentValue, handleChange]; }
把这个钩子整合到Switch组件中,就可以同时支持两种使用方式:
function Switch({ checked: controlledChecked, onChange: controlledOnChange, defaultChecked = false, children }) { const [checked, onChange] = useControllable( controlledChecked, controlledOnChange, defaultChecked ); return children({ checked, onChange }); } // 非受控用法(内部维护状态) <Switch> {({ checked }) => /* 自定义UI */} </Switch> // 受控用法(外部控制状态) const [enabled, setEnabled] = useState(false); <Switch checked={enabled} onChange={setEnabled}> {({ checked }) => /* 自定义UI */} </Switch>
第三步:对应Tailwind源码的核心逻辑
Tailwind的源码实现只是在基础逻辑上做了通用封装:
useControllable:和我们实现的逻辑一致,统一处理受控/非受控状态,拿到最终的checked和onChange。slot = useMemo(() => ({ checked }), [checked]):将需要暴露的状态缓存,避免不必要的重渲染。render(...)函数:这是一个通用渲染工具,负责处理用户传入的as属性(比如as={Fragment})、合并组件内部props和用户props,最终调用用户传入的children函数并传入slot中的状态——本质和我们基础版里的children({ checked })完全一致,只是把通用逻辑抽离出来复用。
完整复刻Tailwind式Switch(带隐藏原生input)
如果需要支持表单提交等场景,可以加上隐藏的原生checkbox:
import { useState } from 'react'; function useControllable(controlledValue, onChange, defaultValue) { const [internalValue, setInternalValue] = useState(defaultValue); const isControlled = controlledValue !== undefined; const currentValue = isControlled ? controlledValue : internalValue; const handleChange = (newValue) => { onChange?.(newValue); if (!isControlled) setInternalValue(newValue); }; return [currentValue, handleChange]; } function Switch({ checked: controlledChecked, onChange: controlledOnChange, defaultChecked = false, name, as = 'div', children }) { const [checked, onChange] = useControllable(controlledChecked, controlledOnChange, defaultChecked); const Tag = as; const handleToggle = () => { onChange(!checked); }; return ( <> {/* 隐藏的原生checkbox,用于表单提交和无障碍支持 */} {name && ( <input type="checkbox" hidden checked={checked} name={name} onChange={() => {}} /> )} {/* 渲染用户自定义UI,传入checked状态 */} <Tag onClick={handleToggle}> {children({ checked })} </Tag> </> ); }
内容的提问来源于stack exchange,提问作者Tommy Wolfheart
相关产品推荐
相关产品推荐

