React项目可复用组件的最优组织方式咨询
问题
我正在开发一个React项目,没有导师指导,不确定当前组件组织方向是否正确。我希望尽可能复用组件,目前组件按类别/视图组织,比如buttons目录下有Refuse、Validate等近十个功能按钮。
当前代码结构示例:
Header组件中调用按钮
import { Refuse } from 'components/buttons/Refuse'; import { Validate } from 'components/buttons/Validate'; export const Header = () => { return ( <> <Refuse offerData={offerData} setValue={setValue} onSubmit={onSubmit} register={register} watch={watch} /> <Validate register={register} setError={setError} offerData={offerData} setValue={setValue} watch={watch} clearErrors={clearErrors} /> </> ); };
Validate按钮组件实现(简化版)
import { Button } from '@mui/material'; import { useDispatch, useSelector } from 'react-redux'; export const Validate = ({ offerData, setError, register }) => { const dispatch = useDispatch(); const isLoading = useSelector(state => state?.orders.loading); const userRole = useSelector(state => state.user?.userGroup); return ( <> {/* 大量权限、状态判断逻辑 */} {userRole?.includes('1') && userRole?.includes('3') ? ( <Button disabled={[1,3,4].includes(offerData?.Status)} onClick={() => {/* 业务逻辑 */}} > {isLoading ? 'Chargement' : 'Valider'} </Button> ) : ( <Button disabled={/* 另一套权限判断 */} onClick={() => {/* 业务逻辑 */}} > {isLoading ? 'Chargement' : 'Valider'} </Button> )} {/* 关联的弹窗组件 */} </> ); };
我考虑删除这些单功能按钮文件,创建一个通用的ButtonComponent:
import { Button } from '@mui/material'; export const ButtonComponent = ({ type, action, texte, ...props }) => { return ( <Button type={type} onClick={action} {...props}> {texte} </Button> ); };
但这样会导致调用页面的代码量剧增,大量业务逻辑(权限判断、状态禁用、弹窗触发)都要写到Header里。想咨询:组织React可复用组件的最优方式是什么?
回答
1. 先区分「基础UI组件」和「业务组件」
复用的核心不是减少文件数量,而是减少重复逻辑,所以要把组件分成两类:
- 基础UI组件:只负责通用的UI样式和基础交互,不绑定业务逻辑。比如你想做的
ButtonComponent,应该封装MUI Button的通用逻辑(比如加载状态、统一样式、禁用状态的基础处理),供所有业务组件调用。 - 业务组件:封装特定业务场景的逻辑,比如
RefuseButton、ValidateButton,把权限判断、状态校验、弹窗触发、Redux操作这些和业务强相关的逻辑都封装在里面,调用方只需要传必要的业务数据(比如offerData)即可。
2. 优化组件目录结构
可以按「通用UI」和「业务模块」拆分目录,避免混乱:
src/ ├─ components/ │ ├─ ui/ # 基础通用组件 │ │ ├─ ButtonComponent.jsx │ │ ├─ InputComponent.jsx │ │ └─ DialogComponent.jsx │ └─ business/ # 业务绑定组件 │ ├─ orders/ # 订单模块相关组件 │ │ ├─ RefuseButton.jsx │ │ ├─ ValidateButton.jsx │ │ └─ OrderHeader.jsx │ └─ users/ # 用户模块相关组件 └─ utils/ # 通用工具函数 └─ buttonPermissions.js
3. 重构你的按钮组件方案
第一步:封装基础UI Button
先把通用逻辑(比如加载状态、统一禁用处理)放到ButtonComponent里:
import { Button } from '@mui/material'; export const ButtonComponent = ({ isLoading, loadingText = 'Chargement', children, ...props }) => { // 统一处理加载时的禁用状态 const finalDisabled = props.disabled || isLoading; return ( <Button disabled={finalDisabled} {...props}> {isLoading ? loadingText : children} </Button> ); };
第二步:重构业务按钮,抽离重复逻辑
把权限判断、状态校验这些重复逻辑抽到工具函数里,比如utils/buttonPermissions.js:
// 判断Refuse按钮是否应该禁用 export const shouldDisableRefuseButton = (userRole, offerData, isLoading) => { if (isLoading) return true; if (userRole?.includes('1') && userRole?.includes('3')) { return ![0, 2].includes(offerData?.Status); } if (userRole?.includes('1') && !userRole?.includes('2')) { return offerData?.Status !== 0; } if (userRole?.includes('2') && !userRole?.includes('1')) { return offerData?.Status !== 1; } if (userRole?.includes('3')) { return offerData?.Status !== 2; } return true; }; // 判断Validate按钮是否应该禁用 export const shouldDisableValidateButton = (userRole, offerData, isLoading) => { if (isLoading) return true; if (userRole?.includes('1') && userRole?.includes('3')) { return [1,3,4].includes(offerData?.Status); } if (userRole?.includes('1')) { return offerData?.Status !== 0; } if (userRole?.includes('2')) { return offerData?.Status !== 1; } if (userRole?.includes('3')) { return offerData?.Status !== 2; } return true; };
然后重构RefuseButton业务组件:
import { useDispatch, useSelector } from 'react-redux'; import { setActiveButton } from '../../features/buttons.slice'; import { setOpen } from '../../features/popup.slice'; import { ButtonComponent } from '../../ui/ButtonComponent'; import { FormDialog } from '../popup'; import { shouldDisableRefuseButton } from '../../utils/buttonPermissions'; export const RefuseButton = ({ register, watch, setValue, offerData, errors }) => { const dispatch = useDispatch(); const isLoading = useSelector(state => state?.orders.loading); const userRole = useSelector(state => state.user?.userGroup); // 调用工具函数判断禁用状态 const isDisabled = shouldDisableRefuseButton(userRole, offerData, isLoading); const handleClick = () => { dispatch(setOpen(true)); dispatch(setActiveButton('Refuser')); }; return ( <> <ButtonComponent variant="text" type="button" onClick={handleClick} isLoading={isLoading} disabled={isDisabled} > Refuser </ButtonComponent> <FormDialog offerData={offerData} register={register} watch={watch} setValue={setValue} errors={errors} /> </> ); };
第三步:调用业务组件更简洁
Header里调用时,只需要传必要的业务数据,逻辑都封装在组件内部:
import { RefuseButton } from 'components/business/orders/RefuseButton'; import { ValidateButton } from 'components/business/orders/ValidateButton'; export const Header = () => { return ( <> <RefuseButton offerData={offerData} setValue={setValue} register={register} watch={watch} errors={errors} /> <ValidateButton offerData={offerData} setError={setError} register={register} setValue={setValue} watch={watch} clearErrors={clearErrors} /> </> ); };
4. 核心原则总结
- 不要把业务逻辑扩散到页面:如果多个页面需要用到同一个按钮,把逻辑封装在业务组件里,改逻辑时只需要改一处。
- 基础组件做通用,业务组件做专属:基础组件负责统一UI和通用交互,业务组件绑定特定场景的逻辑,两者配合才能最大化复用。
- 抽离重复逻辑到工具函数:组件里的判断逻辑、计算逻辑尽量抽到工具函数,让组件代码更简洁,也方便复用和测试。
内容的提问来源于stack exchange,提问作者Elodie
相关产品推荐
相关产品推荐

