遇到的問題
嘗試用 attrs 作 props mapping 封裝,讓封裝後的 components 在使用時,可以直接傳定義在 theme
當中的 colorName
:
const BaseText = styled.div`color: ${(props) => props.$color};// ... other styles`;const ArticleText = styled(Message).attrs((props) => ({$color: props.theme.colors[props.colorName],}))`// ... some other styles`;
如此就可以像這樣使用,<ArticleText>
最終產生的 <div>
會拿到 theme
object 中定義的 red-100
的顏色值:
// (O) THIS WORKS FINE<ArticleText colorName="red-100" {...props}>
而當想要封裝共用某個同樣顏色的 SpecialArticleText
,原本想說可以用 .attrs()
方法一律傳入 colorName: 'red-100'
,但發現並沒有效果:
// (X) THIS DOES NOT WORKconst SpecialArticleText = styled(ArticleText).attrs({ colorName: 'red-100' })`// ...some other styles`<SpecialArticleText>{content}</SpecialArticleText>
探討原因
仔細看了 .attrs()
的原始碼就知道原因。
假如我們有這樣封裝的 components:
const BaseDiv = styled.div`/* ... */`;const StyledBaseDiv = styled(BaseDiv).attrs(attrsOfStyledBaseDiv)`/* ... */`;const StyledStyledBaseDiv = styled(StyledBaseDiv).attrs(attrsOfStyledStyledBaseDiv)`/* ... */`;
其中 attrsOfStyledBaseDiv
和 attrsOfStyledStyledBaseDiv
都是作 props mapping 的函數。
那麼在 render StyledStyledBaseDiv
時,styled-components
v5.3.6 處理會是類似像這樣的邏輯,
// outerProps are the props passed to <StyledStyledBaseDiv>const copiedPropsWithTheme = { ...outerProps, theme };const resolvedAttrs = {};[attrsOfStyledBaseDiv, attrsOfStyledStyledBaseDiv].forEach((attrDef) => {let resolvedAttrDef = attrDef;let key;if (isFunction(resolvedAttrDef)) {// attrs 參數可以是 `Object` 或 `() => Object`resolvedAttrDef = resolvedAttrDef(copiedPropsWithTheme);}for (key in resolvedAttrDef) {copiedPropsWithTheme[key] = resolvedAttrs[key] =key === "className"? joinStrings(resolvedAttrs[key], resolvedAttrDef[key]): resolvedAttrDef[key];}});const computedProps = {...outerProps,...resolvedAttrs,};// filter out $xxx, as, shouldForwardProps() === false ...const propsToElement = propsFilters(computedProps);return <div {...propsToElement} />;
可以看到:
- attrs 參數的初始拿到的 props 是傳給最外層的 outerProps
- 但 attrs 參數的 mapping 順序是由內層到外層
- 最後的 computedProps 是用 mapping 的結果覆蓋掉給最外層的 outerProps
回到前面的例子:
const BaseText = styled.div`color: ${props => props.$color};// ... other styles`const ArticleText = styled(Message).attrs(props => ({$color: props.theme.colors[props.colorName]}))`// ... some other styles`// (X) THIS DOES NOT WORKconst SpecialArticleText = styled(ArticleText).attrs({ colorName: 'red-100' })`// ...some other styles`<SpecialArticleText>{content}</SpecialArticleText>
在這樣的情況下,會先執行 ArticleText
的 attrsDef
,才用 SpecialArticleText
的 attrs
更新 props,跟想像的順序是反過來的。
更一般化的範例:
const BaseDiv = styled.div``;// StyledBaseDiv:const attrsOfStyledBaseDiv = (props) => {console.log("StyledBaseDiv attrs", { ...props });return {_viaStyledBaseDiv: true,value: "StyledBaseDiv",};};const StyledBaseDiv = styled(BaseDiv).attrs(attrsOfStyledBaseDiv)``;// StyledStyledBaseDiv:const attrsOfStyledStyledBaseDiv = (props) => {console.log("StyledStyledBaseDiv attrs", { ...props });return {_viaStyledStyledBaseDiv: true,value: "StyledStyledBaseDiv",};};const StyledStyledBaseDiv = styled(StyledBaseDiv).attrs(attrsOfStyledStyledBaseDiv)``;// StyledStyledStyledBaseDivconst attrsOfStyledStyledStyledBaseDiv = (props) => {console.log("StyledStyledStyledBaseDiv attrs", { ...props });return {_viaStyledStyledStyledBaseDiv: true,value: "StyledStyledStyledBaseDiv",};};const StyledStyledStyledBaseDiv = styled(StyledStyledBaseDiv).attrs(attrsOfStyledStyledStyledBaseDiv,)``;// What will be printed to console?return <StyledStyledStyledBaseDiv value="outerProps" />;// Order:// 1. StyledBaseDiv attrs// 2. StyledStyledBaseDiv attrs// 3. StyledStyledStyledBaseDiv attrs// Props gotten in each callback:// In StyledBaseDiv attrs:// {// value: "outerProps"// }// In StyledStyledBaseDiv attrs:// {// _viaStyledBaseDiv: true,// value: "StyledBaseDiv"// }// In StyledStyledStyledBaseDiv attrs:// {// _viaStyledBaseDiv: true,// _viaStyledStyledBaseDiv: true,// value: "StyledStyledBaseDiv"// }// And the <div> will get// {// _viaStyledBaseDiv: true,// _viaStyledStyledBaseDiv: true,// _viaStyledStyledStyledBaseDiv: true,// value: "StyledStyledStyledBaseDiv"// }
小結
- 可以看到:
- 當 styled 多層時,會從外面傳的 props 開始,從最裡面的
attrs
開始跑 props mapping - 外面的
attrs
function return 的結果會蓋掉裡面的結果(包括 return 的 object 裡值是undefined
的 property)。而非外面的 props 會傳給裡面的(跟 component 封裝不同) - 過濾掉 props 不傳給 element,是所有 mapping 全部跑完才做,包括
$xxx
這種 props。而非一層一層過濾。 - 每一層的
attrs
接到的 props 是會被後面執行的attrs
mutate 的,直接 console 出來會是最後的結果
- 當 styled 多層時,會從外面傳的 props 開始,從最裡面的
- styled-component 6 create component 和 attrs 都改寫,不確定是否有動到邏輯
結論
- 當把
.attrs
用在styled(StyledComponent)
時,它其實不是在指定這一層的 props,而是在覆蓋前面的封裝設定過要傳給 element 的 props,而且當使用 function 時,它可以拿到前面的封裝設定完的 props 作為參數。 - 當在寫一個 component 時,如果是想要它 map 外面傳來的 prop,用一般的 function component 或 tagged template literal 都比較安全,不會遇到前面再被
styled().attrs()
時會不如預期的問題 - 目前沒想到有什麼 use case 是適合用
.attrs
傳 function - 使用
shouldForwardProps
也要小心,一個 prop 要每一層的shouldForwardProps
都通過才會傳給 element,且每一層的shouldForwardProps
參數接到的 props 都是跑過全部的attrs
mapped 完之後的 props
Ref
- attrs https://github.com/styled-components/styled-components/blob/v5.3.6/packages/styled-components/src/models/StyledComponent.js#L48-L74
- config.shouldForwardProps https://github.com/styled-components/styled-components/blob/v5.3.6/packages/styled-components/src/models/StyledComponent.js#L211-L226
- https://styled-components.com/docs/api#attrs