创新并不是在哪都能发芽

最近在写一个需求的时候,要实现一个吸顶功能折叠面板组件,看起来并不是很难,但是如何去设计一个组件,我觉得是比较有意思的一件事情,当真正完成的时候,来到代码评审阶段,组长却表示这是没有意义的创新,让我改用最简单的做法去写就好,原来创新并不是在哪都能发芽。

我的设计

一般最好的一个代码写法,去参考组件库,肯定没有错。于是打开 antd、arco 看看他们的实现,antd 写得太复杂,所以主要参考 arco 的折叠组件,我发现他的用法是使用 headless 的写法,之前我也看了 chakra-ui 的组件库,我觉得这种写法挺好的,并没有耦合在一起。

tsx 复制代码
import { Collapse, Divider } from "@arco-design/web-react";
const CollapseItem = Collapse.Item;

const App = () => {
  return (
    <Collapse defaultActiveKey={["1", "2"]} style={{ maxWidth: 1180 }}>
      <CollapseItem header="Beijing Toutiao Technology Co., Ltd." name="1">
        Beijing Toutiao Technology Co., Ltd.
        <Divider style={{ margin: "8px 0" }} />
        Beijing Toutiao Technology Co., Ltd.
        <Divider style={{ margin: "8px 0" }} />
        Beijing Toutiao Technology Co., Ltd.
      </CollapseItem>

      <CollapseItem header="Introduce" name="2" disabled>
        ByteDance's core product, Toutiao ('Headlines'), is a content platform
        in China and around the world. Toutiao started out as a news
        recommendation engine and gradually evolved into a platform delivering
        content in various formats, such as texts, images, question-and-answer
        posts, microblogs, and videos.
      </CollapseItem>
    </Collapse>
  );
};

于是我也想着把我的折叠组件的用法也设计成这样,Collapse 组件可以统一管理 item 的折叠状态,这样就不用交给用户去书写更多的逻辑,但是 item 组件又可以高度自由化,无论是 ui 的样式,还是状态的传输。

滚动吸顶

之前就记得有个 css 属性能够实现滚动到该元素位置时,就给予吸顶,sticky 粘性定位,这个很好做到,目前这个属性的兼容性也还可以,所以就一旦点击后,就把这个 sticky 加上就可以。不过怎么做到滚动到超出这个元素的时候,他又不吸顶,这到底怎么做呢。其实,很简单,只要我们把 html 的布局结构改下就好

html 复制代码
<ul>
  <li class="sticky">sticky 1</li>
  <li>2</li>
  <li>3</li>
  <li>4</li>
</ul>
<ul>
  <li class="sticky">sticky 5</li>
  <li>6</li>
  <li>7</li>
  <li>8</li>
</ul>
<ul>
  <li class="sticky">sticky 9</li>
  <li>10</li>
  <li>11</li>
  <li>12</li>
</ul>
<ul>
  <li class="sticky">sticky 13</li>
  <li>14</li>
  <li>15</li>
  <li>16</li>
</ul>

当他滚动到对应 ul 下的 li 标签 sticky,超过这个 ul 时就会到第二个元素 sticky。

collapse-sticky

准备工作都已经完成,接下来设计上层父组件 collapse-sticky,发现这里有用到 context 的东西,这是一个很妙的点,因为目前的设计用法,并不能通过 props 下发折叠态的数据到 item 组件,item 组件是在业务层面进行引入的,他并不能拿到 collapse-sticky 的状态,并下发到 item 组件,所以这里是必须得用 context 了,而 arco 的 context 写的并不优雅,这里,我借用 chakra-ui 的 context 封装,他写的挺好

ts 复制代码
import {
  createContext as createReactContext,
  useContext as useReactContext,
} from "react";

export interface CreateContextOptions<T> {
  strict?: boolean;
  hookName?: string;
  providerName?: string;
  errorMessage?: string;
  name?: string;
  defaultValue?: T;
}

export type CreateContextReturn<T> = [
  React.Provider<T>,
  () => T,
  React.Context<T>
];

function getErrorMessage(hook: string, provider: string) {
  return `${hook} returned \`undefined\`. Seems you forgot to wrap component within ${provider}`;
}

export function createContext<T>(options: CreateContextOptions<T> = {}) {
  const {
    name,
    strict = true,
    hookName = "useContext",
    providerName = "Provider",
    errorMessage,
    defaultValue,
  } = options;

  const Context = createReactContext<T | undefined>(defaultValue);

  Context.displayName = name;

  function useContext() {
    const context = useReactContext(Context);

    if (!context && strict) {
      const error = new Error(
        errorMessage ?? getErrorMessage(hookName, providerName)
      );
      error.name = "ContextError";
      Error.captureStackTrace?.(error, useContext);
      throw error;
    }

    return context;
  }

  return [Context.Provider, useContext, Context] as CreateContextReturn<T>;
}

使用

ts 复制代码
import { createContext } from "@/hooks/react-context";

interface UseCollapseStickyType {
  activeKeys: string | string[];
  onToggle: (key: string, e) => Promise<any>;
}

export const [CollapseStickyProvider, useCollapseStickyContext] =
  createContext<UseCollapseStickyType>({
    name: "CollapseStickyContext",
    hookName: "useCollapseStickyContext",
    providerName: "<CollapseSticky />",
  });

CollapseSticky 组件,统一管理 item 组件的状态,及提供一个点击的事件的回调传输

tsx 复制代码
import {
  createElement as h,
  PropsWithChildren,
  FC,
  useState,
  useLayoutEffect,
} from "react";
import { CollapseStickyProvider } from "./collapse-context";
import CollapseStickyItem from "./item";

interface Props {
  /**
   * 当前面板选中值
   */
  activeKey?: string | string[];
  /**
   * 折叠面板点击改变事件
   */
  onChange?: (
    key: string,
    newActiveKeys: string[],
    e: React.MouseEvent<Element, MouseEvent>
  ) => Promise<any>;
}

const CollapseSticky: FC<PropsWithChildren<Props>> = ({
  children,
  activeKey,
  onChange,
}) => {
  const [activeKeys, setActiveKeys] = useState(activeKey || []);

  const onItemClick = async (
    key: string,
    e: React.MouseEvent<Element, MouseEvent>
  ) => {
    // 折叠子项点击的事件钩子
  };

  return (
    <CollapseStickyProvider value={{ activeKeys, onToggle: onItemClick }}>
      {children || null}
    </CollapseStickyProvider>
  );
};

const CollapseComponent = CollapseSticky as typeof CollapseSticky & {
  Item: typeof CollapseStickyItem;
};

CollapseComponent.Item = CollapseStickyItem;

export default CollapseComponent;

collapse-sticky-item

collapse-sticky-item 组件,能够具备展开折叠的功能,并能 sticky,而且他的状态是由 collapse-sticky 的 context 下发管理的

tsx 复制代码
import {
  createElement as h,
  PropsWithChildren,
  FC,
  useState,
  useMemo,
  ReactNode,
} from "react";
import cs from "classnames";
import { isArray } from "@/utils/is";
import { useCollapseStickyContext } from "./collapse-context";
import css from "./style.less";

interface Props {
  /**
   * 折叠面板头部内容,允许自定义
   */
  header?: ReactNode | string;
  /**
   * 对应 activeKey,当前面板组件的的唯一标识
   */
  name: string;
}

const Item: FC<PropsWithChildren<Props>> = ({ children, header, name }) => {
  const { activeKeys, onToggle } = useCollapseStickyContext();
  const [loading, setLoading] = useState(false);
  const isExpanded = useMemo(
    () =>
      isArray(activeKeys)
        ? activeKeys?.indexOf(name) > -1
        : activeKeys === name,
    [activeKeys, name]
  );

  const handleClick = (e: React.MouseEvent<Element, MouseEvent>) => {
    onToggle(name, e).finally(() => {});
  };

  return (
    <ul className={cs(!isExpanded ? css.hidden : "")}>
      <li
        className={cs(isExpanded ? css.sticky : "")}
        onClick={handleClick}
        data-expand-name={`${Number(isExpanded)}_${name}`}
      >
        <i className={cs(css.icon, isExpanded ? css.active : "")} />
        {header}
      </li>
      {children || null}
    </ul>
  );
};

export default Item;

用法

在业务层面,我们可以引入这 collapse-sticky 的组件,由于 item 组件本身就在 collapse-sticky 暴露,所以不需要额外引入 item 组件,只需要示例化出来就好。

tsx 复制代码
<CollapseSticky activeKey={activeKey} onChange={handleChange}>
  {collapseList.map((item, index) => (
    <CollapseStickyItem key={item.id} header={item.title} name={`${index}`}>
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
    </CollapseStickyItem>
  ))}
</CollapseSticky>

思考

最终我的版本已经大致输出完成,而在面对组长的评审时,他的原话是,为什么要新增一个 context 的概念,项目中已经有 store 去管理这种全局状态,又新增一个 context,是否会带来一个新的偏重的东西,到时候如果别人看到这个,也随便拿去用,怎么办,而且这种写法上,带来一定的复杂度,我们这个项目比较简单,尽量不要用这么复杂去做这种事情,会带来学习成本,还会带来新的心智负担,到时候不好管理,到时候项目里混用 store 跟 context 也不好溯源。 而我的回答是:

  • store 跟 context 的概念要区分清楚,我的理解上,store 是管理项目的全局状态,组件的局部全局状态是并非得用 store,而且公共组件并不会使用 store 这种第三方依赖库去管理全局状态,用 context 也不会有问题,而且当前这种组件写法的场景也只能用 context
  • headless 组件优点本来就是高度拓展,ui 样式可以自由定制,虽然目前项目并没有接入原子 css,并不足以打开 headless 的全功能,缺点就是可能需要二次封装一个组件,使其成为一个业务组件去使用,使用上肯定是会复杂些

这个项目并不是我的项目,所以,还是会听从上面的安排,把他写成他期望的样子,不再分开 CollapseStickyItem,都把他融合了在 CollapseSticky 里面,他大概长这样:

tsx 复制代码
<CollapseSticky
  activeKey={activeKey}
  onChange={handleChange}
  collapseList={data}
></CollapseSticky>

起初这么做的原因还有一个是为了推广一下这种写法,毕竟目前很多组件库都是以这种形式写的,headless 带来的好处是巨大的,未来的事情谁说的准呢,需求永远在改变,谁又能知道未来项目形成的模样。还有像项目中滥用 useCallback,useEffect 的依赖项全部都是用 eslint 的 fix update 一键更新,导致依赖混乱,重渲染的次数几何倍数增长,等等这种问题。也不知道大家如何看待这 2 种写法,到底哪个好,哪个坏,欢迎大胆讨论 😄

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240254 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar4 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing5 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试