创新并不是在哪都能发芽

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

我的设计

一般最好的一个代码写法,去参考组件库,肯定没有错。于是打开 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 种写法,到底哪个好,哪个坏,欢迎大胆讨论 😄

相关推荐
桂月二二4 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062065 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb6 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角6 小时前
CSS 颜色
前端·css
九酒6 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔7 小时前
HTML5 新表单属性详解
前端·html·html5
lee5767 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm
2401_897579657 小时前
AI赋能Flutter开发:ScriptEcho助你高效构建跨端应用
前端·人工智能·flutter
limit for me8 小时前
react上增加错误边界 当存在错误时 不会显示白屏
前端·react.js·前端框架
浏览器爱好者8 小时前
如何构建一个简单的React应用?
前端·react.js·前端框架