---
title: "封裝多層的 styled-components 時，.attrs 執行順序的陷阱"
description: "透過閱讀 styled-components 的原始碼，瞭解 `.attrs()` 的特性與陷阱"
lang: "zh-Hant-TW"
tags: ["styled-components"]
pubDate: 2022-01-11T00:00:00.000Z
---

## 遇到的問題

嘗試用 attrs 作 props mapping 封裝，讓封裝後的 components 在使用時，可以直接傳定義在 `theme` 當中的 `colorName`：

```jsx
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` 的顏色值：

```jsx
// (O) THIS WORKS FINE
<ArticleText colorName="red-100" {...props}>
```

而當想要封裝共用某個同樣顏色的 `SpecialArticleText`，原本想說可以用 `.attrs()` 方法一律傳入 `colorName: 'red-100'`，但發現並沒有效果：

```jsx
// (X) THIS DOES NOT WORK
const SpecialArticleText = styled(ArticleText).attrs({ colorName: 'red-100' })`
  // ...some other styles
`
<SpecialArticleText>{content}</SpecialArticleText>
```

## 探討原因

仔細看了 `.attrs()` 的原始碼就知道原因。

假如我們有這樣封裝的 components:

```jsx
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 處理會是類似像這樣的邏輯，

```jsx
// 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} />;
```

可以看到：

1. attrs 參數的初始拿到的 props 是傳給最外層的 outerProps
2. 但 attrs 參數的 mapping 順序是由內層到外層
3. 最後的 computedProps 是用 mapping 的結果覆蓋掉給最外層的 outerProps

回到前面的例子：

```jsx
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 WORK
const SpecialArticleText = styled(ArticleText).attrs({ colorName: 'red-100' })`
  // ...some other styles
`
<SpecialArticleText>{content}</SpecialArticleText>
```

在這樣的情況下，會先執行 `ArticleText` 的 `attrsDef`，才用 `SpecialArticleText` 的 `attrs` 更新 props，跟想像的順序是反過來的。

更一般化的範例：

```jsx
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)``;

// StyledStyledStyledBaseDiv

const 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"
//   }
```

### 小結

- 可以看到:
  1. 當 styled 多層時，會從外面傳的 props 開始，**從最裡面的 `attrs` 開始**跑 props mapping
  2. **外面的 `attrs` function return 的結果會蓋掉裡面的結果**（包括 return 的 object 裡值是 `undefined` 的 property）。而非外面的 props 會傳給裡面的（跟 component 封裝不同）
  3. 過濾掉 props 不傳給 element，**是所有 mapping 全部跑完才做**，包括 `$xxx` 這種 props。**而非一層一層過濾**。
  4. 每一層的 `attrs` 接到的 props 是會被後面執行的 `attrs` mutate 的，直接 console 出來會是最後的結果
- styled-component 6 create component 和 attrs 都改寫，不確定是否有動到邏輯

## 結論

1. 當把 `.attrs` 用在 `styled(StyledComponent)` 時，它其實不是在指定這一層的 props，而是在覆蓋前面的封裝設定過要傳給 element 的 props，而且當使用 function 時，它可以拿到前面的封裝設定完的 props 作為參數。
2. 當在寫一個 component 時，如果是想要它 map 外面傳來的 prop，用一般的 function component 或 tagged template literal 都比較安全，不會遇到前面再被 `styled().attrs()` 時會不如預期的問題
3. 目前沒想到有什麼 use case 是適合用 `.attrs` 傳 function
4. 使用 `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](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://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](https://styled-components.com/docs/api#attrs)
