---
# System prepended metadata

title: React 元件重構實務
tags: [React]

---

---
robots: noindex, nofollow
tags: React
---

# React 元件重構實務

## 多選列表

趕工時常常會寫出很隨便的 UI 元件，例如當你需要一個多選標籤時，可能會這樣寫：

```jsx
const Tag = styled.div`
  border: solid 1px gray;
  text-align: justify;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  margin-right: 8px;
`;

const List = styled.div`
  display: flex;
  justify-content: left;
  align-items: center;
  flex-wrap: wrap;
`;

const TagList = () => (
  <List>
    <Tag>orange</Tag>
    <Tag>apple</Tag>
    <Tag>banana</Tag>
  </List>
);
```

但是你知道，一個列表該怎麼排版，是由父元素決定的，所以你會這樣改寫：

```jsx=
const Tag = styled.div`
  border: solid 1px gray;
  text-align: justify;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
`;

const List = styled.div`
  display: flex;
  justify-content: left;
  align-items: center;
  flex-wrap: wrap;
  
  ${Tag} {
    margin-right: 8px;
    
    &:last-child {
      margin-right: 0;
    }
  }
`;
```

接著你希望可以讓資料決定這個列表長怎樣：

```jsx=
const TagList = ({ tags = [] }) => {
  return (
    <List>
      {tags.map((tag) =>
        <Tag key={tag.id}>{tag.name}</Tag>
      )}
    </List>
  );
};
```

然後讓這個列表可以保存目前的狀態：

```jsx=
const TagList = ({ tags = [], onTagClick = () => void }) => {
  return (
    <List>
      {tags.map((tag) =>
        <Tag
          key={tag.id}
          actived={tag.isSelect}
          onClick={() => onTagClick(tag)}
        >
          {tag.name}
        </Tag>
      )}
    </List>
  );
};
```

但是你也知道，像 `isSelect` 這樣的 UI 狀態，對整個應用程式來說是多餘的，所以想把它藏起來：

```jsx=
const TagList = ({
  tags = [],
  candidates = [],
  onChange = () => void
}) => {
  const tagMap = useMemo(
    () => {
      let result = {};
      for (let t of tags) {
        result[t.id] = true;
      }
      return result;
    },
    [tags],
  );
  
  return (
    <List>
      {candidates.map((tag) =>
        <Tag
          key={tag.id}
          actived={!!tagMap[tag.id]}
          onClick={() => {
            let result = [];
            // 保證輸出為新陣列，且和 candidates 的順序一致
            for (let t of candidates) {
              const checked = tagMap[t.id];
              if (tag.id === t.id) {
                if (!checked) result.push(t);
              } else {
                if (checked) result.push(t);
              }
            }
            onChange(result);
          }}
        >
          {tag.name}
        </Tag>
      )}
    </List>
  );
};
```

最後你得到一個可以這樣用的列表：

```jsx=
const Parent = ({ allTags = [] }) => {
  const [tags, setTags] = useState([]);
  
  return (
    <TagList
      tags={tags}
      candidates={allTags}
      onChange={setTags}
    />
  );
};
```

## 抽離副作用

（待續）

## 巢狀 Route

（待續）
