将样式化组件与复合组件模式一起使用时的依赖循环问题

Dependency cycle issue when using Styled Components alongside compound component pattern

我有一些使用复合组件方法创建的手风琴组件(Ryan Florence 有一篇描述复合组件的精彩演讲 here)。

我在 Styled Components 中设置的 ESLint 规则之一是 import/no-cycle to prevent dependency cycles. As I'm using the compound component methodology as well as the advised way to refer to other components 通过在该文件本身中保持与特定组件相关的所有样式,我触发了 import/no-cycle 警告。

这是我的 Accordion.js 文件。

import React, { useState } from "react";

import AccordionTrigger from "./accordionTrigger";
import AccordionContent from "./accordionContent";

const Accordion = ({ children, show }) => {
  const [isActive, setIsActive] = useState(show);

  const handleTriggerClick = () => {
    setIsActive(!isActive);
  };

  const compoundChildren = React.Children.map(children, child => {
    switch (child.type) {
      case AccordionTrigger:
        return React.cloneElement(child, {
          onClick: handleTriggerClick,
          active: isActive ? 1 : 0,
        });

      case AccordionContent:
        return React.cloneElement(child, {
          show: isActive,
        });

      default:
        return child;
    }
  });

  return <div show={show ? 1 : 0}>{compoundChildren}</div>;
};

export default Accordion;

还有我的 AccordionTrigger.js 文件。

import React from "react";
import styled from "styled-components";

import FauxButton from "../buttons/fauxButton";

import Accordion from "./accordion";
import TopLevelTrigger from "./topLevelTrigger";
import SecondaryLevelTrigger from "./secondaryLevelTrigger";

const Root = styled(FauxButton)`
  ${Accordion} & {
    width: 100%;
    border-bottom: 1px solid ${p => p.theme.greyLight};
  }
`;

const AccordionTrigger = ({ active, children, ...rest }) => {
  const clonedChildren = React.Children.map(children, child => {
    switch (child.type) {
      case TopLevelTrigger:
      case SecondaryLevelTrigger:
        return React.cloneElement(child, {
          active,
        });

      default:
        return child;
    }
  });
  return <Root {...rest}>{clonedChildren}</Root>;
};

export default AccordionTrigger;

我试过的一件事是在 Accordion.js 中定义 AccordionTrigger 样式,就像这样...

const Root = styled.div`
  ${AccordionTrigger} & {
    width: 100%;
    border-bottom: 1px solid ${p => p.theme.greyLight};
  }
`;

const Accordion = ({ children, show }) => {
  ...same logic as before here

  return <Root show={show ? 1 : 0}>{compoundChildren}</Root>;
};

...但这不起作用,样式根本不会添加到 AccordionTrigger 组件中。我知道我可以在 Accordion 组件内部克隆时通过 className 道具简单地添加我自己的 class 然后以这种方式引用它,但我想知道是否有办法防止这个?

简而言之,styled-components 生成 className 必须 应用于 HTML/JSX Element 才能看到样式。此外,组合组件 必须是样式组件 的实例。我的示例 解释了自定义组件样式的基本方法。

因为我没有您的完整代码,所以我从您链接的视频中重新创建了示例。

工作示例(在这种情况下,我在 Tab 中设置 TabContent 样式并更改其 svg 元素):

另外,我的文件结构是这样的,这避免了导入递归问题:

├── src
|   ├── components
|   |   ├── Tab
|   |   |   ├── Tab.js
|   |   |   └── index.js
|   |   |
|   |   ├── TabContent
|   |   |   ├── TabContent.js
|   |   |   └── index.js
|   |   | 
|   |   ├── TabList
|   |   |   └── index.js
|   |   | 
|   |   ├── TabPanel
|   |   |   └── index.js
|   |   | 
|   |   ├── TabPanels
|   |   |   ├── TabPanels.js
|   |   |   └── index.js
|   |   | 
|   |   └── Tabs
|   |       └── index.js
|   └── index.js
|
├── index.js
└── tabs.js

src/components/Tab/Tab.js

import React from "react";
import PropTypes from "prop-types";

const Tab = ({ children, className, disabled, onSelectTab }) => (
  <div className={className} onClick={disabled ? null : onSelectTab}>
    {children}
  </div>
);

Tab.propTypes = {
  className: PropTypes.string.isRequired,
  children: PropTypes.node.isRequired,
  disabled: PropTypes.bool,
  onSelectTab: PropTypes.func.isRequired
};

export default Tab;

src/components/Tab/index.js

import styled from "styled-components";
import Tab from "./Tab";
import TabContent from "../TabContent";

export default styled(Tab)`
  display: inline-block;
  padding: 10px;
  margin: 10px;
  border-bottom: 2px solid;
  border-color: rgba(0, 0, 0, 0.65);
  color: rgba(0, 0, 0, 0.65);
  cursor: pointer;
  -webkit-transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
  ${({ disabled, isActive }) => {
    if (disabled) return `opacity: 0.25; cursor: default;`;
    if (isActive) return `color: #1890ff; border-bottom-color: #1890ff;`;
  }}

  &:hover {
    color: #40a9ff;
    border-bottom-color: #40a9ff;
    ${({ disabled }) =>
      disabled &&
      `color: rgba(0, 0, 0, 0.65); border-bottom-color: rgba(0, 0, 0, 0.65);`};
  }

  ${TabContent} {
    & svg {
      font-size: 13px;
    }
  }
`;

src/components/TabContent/TabContent.js

import React from "react";
import PropTypes from "prop-types";

const TabContent = ({ children, className }) => (
  <div className={className}>{children}</div>
);

TabContent.propTypes = {
  className: PropTypes.string.isRequired,
  children: PropTypes.node.isRequired
};

export default TabContent;

src/components/TabContent/index.js

import styled from "styled-components";
import TabContent from "./TabContent";

export default styled(TabContent)`
  font-size: 20px;
`;

src/components/TabList/index.js

import { Children, cloneElement, useCallback } from "react";
import PropTypes from "prop-types";

const TabList = ({ activeIndex, children, setActiveIndex }) => {
  const handleSelectedTab = useCallback(
    index => {
      setActiveIndex(index);
    },
    [setActiveIndex]
  );

  return Children.map(children, (child, index) =>
    cloneElement(child, {
      isActive: activeIndex === index,
      onSelectTab: () => handleSelectedTab(index)
    })
  );
};

TabList.propTypes = {
  activeIndex: PropTypes.number,
  children: PropTypes.node.isRequired,
  setActiveIndex: PropTypes.func
};

export default TabList;

src/components/TabPanel/index.js

import PropTypes from "prop-types";

const TabPanel = ({ children }) => children;

TabPanel.propTypes = {
  children: PropTypes.node.isRequired
};

export default TabPanel;

src/components/TabPanels/TabPanels.js

import React, { Children } from "react";
import PropTypes from "prop-types";

const TabPanels = ({ activeIndex, children, className }) => (
  <div className={className}>{Children.toArray(children)[activeIndex]}</div>
);

TabPanels.propTypes = {
  activeIndex: PropTypes.number,
  children: PropTypes.node.isRequired,
  setActiveIndex: PropTypes.func
};

export default TabPanels;

src/components/TabPanels/index.js

import styled from "styled-components";
import TabPanels from "./TabPanels";

export default styled(TabPanels)`
  padding: 10px;
`;

src/components/Tabs/Tabs.js

import { Children, cloneElement, useState } from "react";
import PropTypes from "prop-types";
import TabPanels from "../TabPanels";
import TabList from "../TabList";

const Tabs = ({ children }) => {
  const [activeIndex, setActiveIndex] = useState(0);

  return Children.map(children, child => {
    switch (child.type) {
      case TabPanels: {
        return cloneElement(child, { activeIndex });
      }
      case TabList: {
        return cloneElement(child, {
          activeIndex,
          setActiveIndex
        });
      }
      default: {
        return child;
      }
    }
  });
};

Tabs.propTypes = {
  children: PropTypes.node.isRequired
};

export default Tabs;

src/components/index.js

export { default as Tab } from "./Tab";
export { default as TabContent } from "./TabContent";
export { default as TabList } from "./TabList";
export { default as TabPanel } from "./TabPanel";
export { default as TabPanels } from "./TabPanels";
export { default as Tabs } from "./Tabs";

src/index.js

import React from "react";
import ReactDOM from "react-dom";
import {
  Tab,
  TabContent,
  Tabs,
  TabList,
  TabPanels,
  TabPanel
} from "./components";
import tabs from "./tabs";

const App = () => (
  <Tabs>
    <TabList>
      {tabs.map(({ icon, title, disabled }) => (
        <Tab key={title} disabled={disabled}>
          <TabContent>
            {icon} {title}
          </TabContent>
        </Tab>
      ))}
    </TabList>
    <TabPanels>
      {tabs.map(({ title, content }) => (
        <TabPanel key={title}>{content}</TabPanel>
      ))}
    </TabPanels>
  </Tabs>
);

ReactDOM.render(<App />, document.getElementById("root"));

src/tabs.js

import React from "react";
import { FaAppleAlt, FaCarrot, FaLemon } from "react-icons/fa";

export default [
  {
    title: "Apples",
    icon: <FaAppleAlt />,
    content: <p>Apples are delicious.</p>
  },
  {
    title: "Carrots",
    icon: <FaCarrot />,
    content: <p>Carrots are nutritious.</p>,
    disabled: true
  },
  {
    title: "Lemons",
    icon: <FaLemon />,
    content: <p>Lemons are ubiquitous.</p>
  }
];