We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
react 以组件构成页面,极大提高了开发效率和代码维护难度,但是不注重组件设计方法,容易导致难以理解、扩展性差、难以复用的组件。
设计一个组件前,需要考的事情:
对需求的理解是否全面、是否考虑了可能的需求变化,很大程度上决定了组件的是否能重用。
props 多少和复杂程度,决定了组件是否易用。
组件设计的复杂度往往度往往决定是是否可扩展。
另外,控制反转越大,复用性和扩展性越好。
控制反转
控制反转:程序如何工作,不是由开发者控制,而是程序调用者有更多控制权。可简单理解为程序能实现高度定制化,用户能实现他想要的很多需求。程序开发者做得很少,但是提供接口给程序调用者实现很多需求。
回答以上三个问题,人们总结了一些设计组件的思路。
实际开发中,开发完一个组件,遇到新需求变化,我们才来回答第三个问题,此时往往很难得到满意的答案,又开始编写满足新需求的组件,但是不知道组件设计方法,仍然很容易出现之前的问题。可见学习常见的组件设计方法极为必要。
复合组件:多个独立的组件组合使用,这些组件隐式地共享状态和行为。复合组件语法符合自觉,易用,灵活,容易扩展。
常规实现方式
export default function Select({ value = [], onChange, options = [] }) { const [filter, setFilter] = useState('') const [optionList, setOptionList] = useState(options) useEffect(() => { if (filter) { const value = options.filter(item => item.value.toLowerCase().includes(filter.toLowerCase())) setOptionList(value) } }, [filter]) return ( <div className='select-container'> <input type='text' placeholder='支持模糊搜索' value={filter} onChange={event => { setFilter(event.currentTarget.value) }} /> <div className='options-container'> {optionList.map(item => { return ( <label className={value.includes(item.value) ? 'option option-selected' : 'option'} key={item.value}> <input className='checkbox' type='checkbox' checked={value.includes(item.value)} onChange={event => { if (event.currentTarget.checked) onChange([...value, item.value]) else onChange(value => value.filter(val => val !== item.value)) }} /> {item.label} </label> ) })} </div> </div> ) }
用法:
<Select value={value} onChange={setValue} options={[ { value: 'apples', label: 'Apples' }, { value: 'oranges', label: 'Oranges' }, { value: 'Peaches', label: 'Peaches' }, { value: 'Grapes', label: 'Grapes' }, { value: 'Plums', label: 'Plums' }, ]} />
缺点:
这种符合直觉。
<select> <option value="apples">Apples</option> <option value="oranges">Oranges</option> <option value="pears">Pears</option> </select>
不够灵活,比如想要给下拉应用样式,变得复杂。
实现比较复杂
使用复合组件模式改进
操作孩子组件实现
import React, { useState } from 'react' export default function MySelect({ children, value, onChange }) { const [filter, setFilter] = useState('') const newChildren = React.Children.map(children, child => { const newChild = React.cloneElement(child, { filter, selectedValue: value, onChange, }) return newChild }) return ( <div> <input value={filter} onChange={event => setFilter(event.currentTarget.value)} /> {newChildren} </div> ) } MySelect.Option = Option function Option({ value, children, onChange, selectedValue, filter }) { if (!value.toLowerCase().includes(filter.toLowerCase())) return null return ( <label> <input type='checkbox' checked={selectedValue.includes(value)} onChange={event => onChange(value, event.currentTarget.checked)} /> {children} </label> ) }
import React, { useState } from 'react' import Select from './index' export default function Example() { const [value, setValue] = useState([]) return ( <div> <Select value={value} onChange={(optionValue, selected) => { // console.log(optionValue, selected) if (selected) { setValue([...value, optionValue]) } else { setValue(value.filter(ele => ele !== optionValue)) } }}> <Select.Option value='apples'>Apples</Select.Option> <Select.Option value='oranges'>Oranges</Select.Option> <Select.Option value='peaches'>Peaches</Select.Option> <Select.Option value='grapes'>Grapes</Select.Option> <Select.Option value='plums'>Plums</Select.Option> </Select> </div> ) }
缺点
不够灵活:使用了操作孩子组件的方法,限制了 Select 的直接子组件只能是 Option,想要使用 div 包裹 Option ,无法实现。
复制组件,有内存开销。
实现比较复杂,不易懂
使用 context 改进上一个方案
主要代码
import type { ReactNode } from 'react' import { createContext, useState, useContext } from 'react' import '../../components/MySelect/index.less' type selectContextType = { isSelected: ((key: string) => boolean) | null setSelected: ((key: string, selected: boolean) => void) | null filter: string } const initialContext = { isSelected: null, setSelected: null, filter: '', } as const const SelectContext = createContext<selectContextType>(initialContext) SelectContext.displayName = 'SelectContext' // 方便调试,不设置 显示 context type propsType = { value: string[] onChange: (value: string[]) => void children: ReactNode } export default function Select({ children, value, onChange }: propsType) { const [filter, setFilter] = useState('') return ( <SelectContext.Provider // NOTE 公共行为和状态 value={{ isSelected: key => value.includes(key), setSelected: (optionValue, selected) => { if (selected) { onChange([...value, optionValue]) } else { const selectedValue = value.filter(val => val !== optionValue) onChange(selectedValue) } }, filter, }}> <div className='select-container'> <input type='text' placeholder='支持模糊搜索' value={filter} onChange={evt => setFilter(evt.target.value)} /> <div className='options-container'>{children}</div> </div> </SelectContext.Provider> ) } Select.Option = Option type optionPropsType = { children: ReactNode value: string } function Option({ children, value }: optionPropsType) { // NOTE 在后代组件中获取公共的行为和状态 const { isSelected, setSelected, filter } = useContext(SelectContext) if (!value.toLowerCase().includes(filter.toLowerCase())) return null return ( <label className={isSelected!(value) ? 'option option-selected' : 'option'}> <input type='checkbox' className='checkbox' checked={isSelected!(value)} onChange={evt => setSelected!(value, evt.currentTarget.checked)} /> {children} </label> ) }
<Select value={selection} onChange={setSelection}> <Select.Option value='apples'>Apples</Select.Option> <Select.Option value='oranges'>Oranges</Select.Option> <Select.Option value='peaches'>Peaches</Select.Option> <Select.Option value='grapes'>Grapes</Select.Option> <Select.Option value='plums'>Plums</Select.Option> </Select> <Select value={selection} onChange={setSelection}> <div style={{ // 可方便地修改样式 display: 'flex', width: '500px', justifyContent: 'flex-start', }} > <Select.Option value='apples'>Apples</Select.Option> <Select.Option value='oranges'>Oranges</Select.Option> <Select.Option value='peaches'>Peaches</Select.Option> <Select.Option value='grapes'>Grapes</Select.Option> <Select.Option value='plums'>Plums</Select.Option> </div> </Select>
优点:
使用前问问自己:
两个以上组件能更好地实现需求吗?设计良好的单个组件可满足当前需求吗?未来需求变化大吗?
采用复合组件实现易用吗?数据交互简单吗?
一个父组件和多个子组件一起使用能更好实现的需求。
<Table> <Row></Row> </Table>
表单
滚动分页
<BestScroll> <Table> <Row></Row> </Table> </BestScroll>
下拉多选、复选框、单选
Tab 标签
菜单
复合组件适合多个组件配合才能完成的需求。
角色划分
复合组件维护自己地状态,是一个非受控组件,但是实际业务中,组件的使用者需要组件的当前状态做其他业务,因此,复合组件的状态最好能被组件外部控制,将复合组件改成受控组件。
import React, { useEffect, useRef, useState, useCallback } from 'react' import styled from 'styled-components' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { library } from '@fortawesome/fontawesome-svg-core' import { faPlus, faMinus } from '@fortawesome/free-solid-svg-icons' import useCounterContext, { CounterProvider } from './useCounterContext' library.add(faPlus) library.add(faMinus) function Counter({ children, onChange, value = null, initialValue = 0 }) { const [count, setCount] = useState(initialValue) // 判断是否为受控组件 const isControlled = useRef(value !== null && typeof onChange === 'function') useEffect(() => { isControlled.current && onChange(count) }, [count]) const handleIncrement = useCallback(() => { setCount(count => count + 1) }, [1]) const handleDecrement = useCallback(() => { setCount(count => Math.max(0, count - 1)) }, [1]) console.log('Counter render') return ( // FIXME 如何优化性能 会渲染两次 <CounterProvider value={{ count, handleDecrement, handleIncrement }}> <StyledCounter>{children}</StyledCounter> </CounterProvider> ) } export default Counter Counter.Count = Count Counter.Label = Label Counter.Increment = Increment Counter.Decrement = Decrement function Count({ max }) { // NOTE 共享 context const { count } = useCounterContext() // TODO 类似 vue 的计算属性 const hasError = max ? count >= max : false return <StyledCount hasError={hasError}>{count}</StyledCount> } const StyledCount = styled.div` background-color: ${({ hasError }) => (hasError ? '#bd2130' : '#17a2b8')}; color: white; padding: 5px 7px; ` // 中间的 Label function Label({ children }) { // children 是用户传递的内容 return <StyledLabel>{children}</StyledLabel> } const StyledLabel = styled.div` background-color: #e9ecef; color: #495057; padding: 5px 7px; ` // 加减按钮 function Increment({ icon = 'plus' }) { // NOTE 共享行为 const { handleIncrement } = useCounterContext() console.log('Increment') return ( <StyledButton onClick={handleIncrement}> <FontAwesomeIcon color='#17a2b8' icon={icon} /> </StyledButton> ) } function Decrement({ icon = 'minus' }) { console.log('Decrement') const { handleDecrement } = useCounterContext() return ( <StyledButton onClick={handleDecrement}> <FontAwesomeIcon color='#17a2b8' icon={icon} /> </StyledButton> ) } const StyledButton = styled.button` background-color: white; border: none; &:hover { cursor: pointer; } &:active, &:focus { outline: none; } ` const StyledCounter = styled.div` display: inline-flex; border: 1px solid #17a2b8; line-height: 1.5; border-radius: 0.25rem; overflow: hidden; `
用法
import React, { useState } from 'react' import Counter from './Counter' export default function Usage() { const [value, setValue] = useState(0) return ( <div> <h3>受控组件</h3> <Counter value={value} onChange={value => { console.log(value) setValue(value) }}> <Counter.Decrement /> <Counter.Label>计数器</Counter.Label> <Counter.Count max={10} /> <Counter.Increment /> </Counter> <hr /> <Counter> <Counter.Decrement /> <Counter.Increment /> <Counter.Count max={10} /> <Counter.Label>counter</Counter.Label> </Counter> </div> ) }
缺点: 复合组件的缺点它都有。
Making good component design decisions in React
Quick guide to React compound components
Advanced React Component Patterns
React Hooks: Compound Components
How To Master Advanced React Design Patterns — Compound Components
The text was updated successfully, but these errors were encountered:
No branches or pull requests
如何设计一个组件?常见的 React 组件设计模式 --- 复合组件
react 以组件构成页面,极大提高了开发效率和代码维护难度,但是不注重组件设计方法,容易导致难以理解、扩展性差、难以复用的组件。
设计一个组件前,需要考的事情:
另外,
控制反转
越大,复用性和扩展性越好。回答以上三个问题,人们总结了一些设计组件的思路。
实际开发中,开发完一个组件,遇到新需求变化,我们才来回答第三个问题,此时往往很难得到满意的答案,又开始编写满足新需求的组件,但是不知道组件设计方法,仍然很容易出现之前的问题。可见学习常见的组件设计方法极为必要。
复合组件
复合组件:多个独立的组件组合使用,这些组件隐式地共享状态和行为。复合组件语法符合自觉,易用,灵活,容易扩展。
用法:
缺点:
这种符合直觉。
不够灵活,比如想要给下拉应用样式,变得复杂。
实现比较复杂
使用复合组件模式改进
用法:
不够灵活:使用了操作孩子组件的方法,限制了 Select 的直接子组件只能是 Option,想要使用 div 包裹 Option ,无法实现。
复制组件,有内存开销。
实现比较复杂,不易懂
主要代码
用法:
优点:
缺点:
使用复合组件需要注意什么
使用前问问自己:
两个以上组件能更好地实现需求吗?设计良好的单个组件可满足当前需求吗?未来需求变化大吗?
采用复合组件实现易用吗?数据交互简单吗?
哪些场景适合复合组件
一个父组件和多个子组件一起使用能更好实现的需求。
表单
滚动分页
下拉多选、复选框、单选
Tab 标签
菜单
总结
复合组件适合多个组件配合才能完成的需求。
角色划分
如何设计一个组件?常见的 React 组件设计模式 --- control-props
复合组件维护自己地状态,是一个非受控组件,但是实际业务中,组件的使用者需要组件的当前状态做其他业务,因此,复合组件的状态最好能被组件外部控制,将复合组件改成受控组件。
用法
优点:
缺点:
复合组件的缺点它都有。
参考文章
Making good component design decisions in React
Quick guide to React compound components
Advanced React Component Patterns
React Hooks: Compound Components
How To Master Advanced React Design Patterns — Compound Components
The text was updated successfully, but these errors were encountered: