如何向styled-components动态传递样式?高阶组件场景优化
我正在制作一个包含文本和图标的高阶组件,底层使用styled-components进行渲染,希望能动态传递计算后的样式,无需传递大量props。
以下是简化示例,其中size属性需同时用于父组件和CSS中。仅传递size属性不算麻烦,但当需要在父组件和样式中使用大量props时,会出现大量冗余的props传递。
我想到的第一种解决方案是在需要时创建新的styled.button,并复用父组件的size属性:
import { FC } from 'react' import { IconType } from 'react-icons' import styled from 'styled-components' const Button: FC<{ icon?: IconType text?: string size?: 'sm' | 'md' | 'lg' onClick?: React.MouseEventHandler }> = ({ icon: Icon, text, size, onClick }) => { const iconSize = size === 'sm' ? 22 : size === 'md' ? 26 : 30 const iconElem = Icon && <Icon size={iconSize} /> const StyledButton = styled.button` display: flex; align-items: center; justify-content: center; gap: 1rem; width: fit-content; padding: 0.55rem ${size === 'sm' ? 0.75 : size === 'md' ? 1 : 1.25}rem; font-size: ${size === 'sm' ? 1 : size === 'md' ? 1.125 : 1.25}rem; border-radius: ${size === 'sm' ? 0.5 : size === 'md' ? 0.55 : 0.65}rem; font-weight: ${props => props.theme.fontWeights.semiBold}; ` return ( <StyledButton onClick={onClick}> {iconElem} {text} </StyledButton> ) } export default Button
但styled-components不允许这样动态创建组件,会抛出警告:
The component Button__StyledButton with the id of "Button__StyledButton-sc-3866117b-0" has been created dynamically.
You may see this warning because you've called styled inside another component.
To resolve this only create new StyledComponents outside of any render method and function component.
于是我尝试了如下方法,虽然可行,但略显繁琐冗余:
import { FC } from 'react' import { IconType } from 'react-icons' import styled, { css, FlattenInterpolation, ThemeProps, DefaultTheme } from 'styled-components' const Button: FC<{ icon?: IconType text?: string size?: 'sm' | 'md' | 'lg' onClick?: React.MouseEventHandler }> = ({ icon: Icon, text, size, onClick }) => { const iconSize = size === 'sm' ? 22 : size === 'md' ? 26 : 30 const iconElem = Icon && <Icon size={iconSize} /> const styles = css` display: flex; align-items: center; justify-content: center; gap: 1rem; width: fit-content; padding: 0.55rem ${size === 'sm' ? 0.75 : size === 'md' ? 1 : 1.25}rem; font-size: ${size === 'sm' ? 1 : size === 'md' ? 1.125 : 1.25}rem; border-radius: ${size === 'sm' ? 0.5 : size === 'md' ? 0.55 : 0.65}rem; font-weight: ${props => props.theme.fontWeights.semiBold}; ` return ( <StyledButton styles={styles} onClick={onClick}> {iconElem} {text} </StyledButton> ) } const StyledButton = styled.button<{ styles: FlattenInterpolation<ThemeProps<DefaultTheme>> }>` ${props => props.styles} ` export default Button
请问是否有更优的实现方式?
Atlassian的@compiled/react库支持直接为元素添加css样式,示例如下:
const redText = css` color: red; `; <div css={redText} />;
styled-components是否具备类似功能?
方案1:将StyledButton移至组件外部,通过props传递参数
这是最符合styled-components设计规范的方式,把样式逻辑完全放在外部的styled组件中,仅传递必要的props:
import { FC } from 'react' import { IconType } from 'react-icons' import styled from 'styled-components' // 外部定义StyledButton,接收size和theme props const StyledButton = styled.button<{ size?: 'sm' | 'md' | 'lg' }>` display: flex; align-items: center; justify-content: center; gap: 1rem; width: fit-content; padding: 0.55rem ${props => props.size === 'sm' ? '0.75rem' : props.size === 'md' ? '1rem' : '1.25rem'}; font-size: ${props => props.size === 'sm' ? '1rem' : props.size === 'md' ? '1.125rem' : '1.25rem'}; border-radius: ${props => props.size === 'sm' ? '0.5rem' : props.size === 'md' ? '0.55rem' : '0.65rem'}; font-weight: ${props => props.theme.fontWeights.semiBold}; ` const Button: FC<{ icon?: IconType text?: string size?: 'sm' | 'md' | 'lg' onClick?: React.MouseEventHandler }> = ({ icon: Icon, text, size, onClick }) => { const iconSize = size === 'sm' ? 22 : size === 'md' ? 26 : 30 const iconElem = Icon && <Icon size={iconSize} /> return ( <StyledButton size={size} onClick={onClick}> {iconElem} {text} </StyledButton> ) } export default Button
如果需要复用size的计算逻辑,可以抽成单独的工具函数,进一步简化代码:
// 抽离size计算逻辑 const getSizeConfig = (size?: 'sm' | 'md' | 'lg') => { switch(size) { case 'sm': return { icon: 22, paddingX: '0.75rem', fontSize: '1rem', borderRadius: '0.5rem' } case 'md': return { icon: 26, paddingX: '1rem', fontSize: '1.125rem', borderRadius: '0.55rem' } case 'lg': return { icon: 30, paddingX: '1.25rem', fontSize: '1.25rem', borderRadius: '0.65rem' } default: return { icon: 26, paddingX: '1rem', fontSize: '1.125rem', borderRadius: '0.55rem' } } } // 在StyledButton中使用统一的配置 const StyledButton = styled.button<{ sizeConfig: ReturnType<typeof getSizeConfig> }>` display: flex; align-items: center; justify-content: center; gap: 1rem; width: fit-content; padding: 0.55rem ${props => props.sizeConfig.paddingX}; font-size: ${props => props.sizeConfig.fontSize}; border-radius: ${props => props.sizeConfig.borderRadius}; font-weight: ${props => props.theme.fontWeights.semiBold}; ` // 组件内调用 const Button: FC<{ icon?: IconType text?: string size?: 'sm' | 'md' | 'lg' onClick?: React.MouseEventHandler }> = ({ icon: Icon, text, size, onClick }) => { const sizeConfig = getSizeConfig(size) const iconElem = Icon && <Icon size={sizeConfig.icon} /> return ( <StyledButton sizeConfig={sizeConfig} onClick={onClick}> {iconElem} {text} </StyledButton> ) }
方案2:使用styled-components的css prop(类似@compiled/react)
styled-components从v5版本开始支持css prop,允许直接给元素添加样式,用法和@compiled/react一致,但需要提前配置:
配置方式
- 安装
babel-plugin-styled-components并在babel配置中启用cssProp选项; - 若使用TypeScript,需在
tsconfig.json中添加"types": ["styled-components/cssprop"];
示例代码
import { FC } from 'react' import { IconType } from 'react-icons' import { css } from 'styled-components' const Button: FC<{ icon?: IconType text?: string size?: 'sm' | 'md' | 'lg' onClick?: React.MouseEventHandler }> = ({ icon: Icon, text, size, onClick }) => { const iconSize = size === 'sm' ? 22 : size === 'md' ? 26 : 30 const iconElem = Icon && <Icon size={iconSize} /> const buttonStyles = css` display: flex; align-items: center; justify-content: center; gap: 1rem; width: fit-content; padding: 0.55rem ${size === 'sm' ? '0.75rem' : size === 'md' ? '1rem' : '1.25rem'}; font-size: ${size === 'sm' ? '1rem' : size === 'md' ? '1.125rem' : '1.25rem'}; border-radius: ${size === 'sm' ? '0.5rem' : size === 'md' ? '0.55rem' : '0.65rem'}; font-weight: ${props => props.theme.fontWeights.semiBold}; ` return ( <button css={buttonStyles} onClick={onClick}> {iconElem} {text} </button> ) } export default Button
这种方式无需额外创建styled组件,直接给原生元素添加css prop,同时能正常访问theme变量。
方案3:使用attrs方法统一处理props
如果需要给styled组件设置默认props或者统一处理属性,可以用attrs方法:
import { FC } from 'react' import { IconType } from 'react-icons' import styled from 'styled-components' const StyledButton = styled.button.attrs<{ size?: 'sm' | 'md' | 'lg' }>(props => ({ // 可在此设置默认props或计算属性 }))` display: flex; align-items: center; justify-content: center; gap: 1rem; width: fit-content; padding: 0.55rem ${props => props.size === 'sm' ? '0.75rem' : props.size === 'md' ? '1rem' : '1.25rem'}; font-size: ${props => props.size === 'sm' ? '1rem' : props.size === 'md' ? '1.125rem' : '1.25rem'}; border-radius: ${props => props.size === 'sm' ? '0.5rem' : props.size === 'md' ? '0.55rem' : '0.65rem'}; font-weight: ${props => props.theme.fontWeights.semiBold}; ` const Button: FC<{ icon?: IconType text?: string size?: 'sm' | 'md' | 'lg' onClick?: React.MouseEventHandler }> = ({ icon: Icon, text, size, onClick }) => { const iconSize = size === 'sm' ? 22 : size === 'md' ? 26 : 30 const iconElem = Icon && <Icon size={iconSize} /> return ( <StyledButton size={size} onClick={onClick}> {iconElem} {text} </StyledButton> ) }
内容的提问来源于stack exchange,提问作者Wowkster

