react-use#useDeepCompareEffect TypeScript Examples

The following examples show how to use react-use#useDeepCompareEffect. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: useEncryptedStore.ts    From back-home-safe with GNU General Public License v3.0 4 votes vote down vote up
useEncryptedStore = <T extends T[] | Object>({
  key,
  defaultValue: fallbackValue,
  passthroughOnly = false,
}: {
  key: string;
  defaultValue: T;
  passthroughOnly?: boolean;
}) => {
  const [incognito, setIncognito] = useLocalStorage("incognito", false);
  const [defaultValue] = useState<T>(fallbackValue);

  const [password, setPassword] = useState<string | null>(null);
  const [savedValue, setSavedValue, removeSavedValue] = useLocalStorage<string>(
    key,
    passthroughOnly
      ? undefined
      : password
      ? encryptValue(JSON.stringify(defaultValue), password)
      : JSON.stringify(defaultValue)
  );

  const isEncrypted = useMemo(() => {
    try {
      if (!savedValue) return false;
      JSON.parse(savedValue);
    } catch (e) {
      return true;
    }
    return false;
  }, [savedValue]);

  const [decryptedValue, setDecryptedValue] = useState<T>(
    !isEncrypted && savedValue ? JSON.parse(savedValue) : defaultValue
  );

  const [unlocked, setUnlocked] = useState(!isEncrypted);

  useDeepCompareEffect(() => {
    if (!unlocked || incognito || passthroughOnly) return;
    if (isEncrypted && !password) return;
    const value = JSON.stringify(decryptedValue);
    console.log(value);
    setSavedValue(password ? encryptValue(value, password) : value);
  }, [decryptedValue]);

  const initPassword = useCallback(
    (newPassword: string) => {
      if (isEncrypted || passthroughOnly) return;
      const data = encryptValue(
        savedValue || JSON.stringify(defaultValue),
        newPassword
      );
      setSavedValue(data);
      setPassword(newPassword);
    },
    [passthroughOnly, savedValue, setSavedValue, defaultValue, isEncrypted]
  );

  const unlockStore = useCallback(
    (password: string) => {
      if (!isEncrypted) return true;
      try {
        const decryptedValue = decryptValue(
          savedValue || JSON.stringify(defaultValue),
          password
        );
        setDecryptedValue(JSON.parse(decryptedValue));
        setPassword(password);
        setUnlocked(true);

        return true;
      } catch (e) {
        return false;
      }
    },
    [defaultValue, savedValue, isEncrypted]
  );

  const lockStore = useCallback(() => {
    if (!isEncrypted) return;
    setUnlocked(false);
    setPassword(null);
    setDecryptedValue(defaultValue);
  }, [isEncrypted, defaultValue]);

  return {
    isEncrypted,
    unlockStore,
    lockStore,
    value: decryptedValue,
    setValue: setDecryptedValue,
    initPassword,
    unlocked,
    incognito,
    setIncognito,
    password,
    destroy: removeSavedValue,
    hasData: !isNil(savedValue),
  };
}
Example #2
Source File: index.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
LoadMoreSelector = (props: ILoadMoreSelectorProps) => {
  const {
    getData,
    pageSize = PAGINATION.pageSize,
    list: allList = emptyArray,
    LoadMoreRender = DefaultLoadMoreRender,
    onDropdownVisible: propsOnDropdownVisible,
    onChangeCategory: changeCategory,
    type,
    dataFormatter = defaultDataFormatter,
    extraQuery = {},
    ...rest
  } = props;

  const [{ q, pageNo, hasMore, chosenCategory, firstReq, list, visible, loading }, updater, update] = useUpdate({
    pageNo: 1,
    q: undefined as string | undefined,
    hasMore: false,
    chosenCategory: undefined as string | undefined,
    firstReq: false,
    list: allList as IOption[],
    visible: false,
    loading: false,
  });

  const isStaticData = !getData; // 是否是静态数据模式
  const isCategoryMode = type === SelectType.Category;

  React.useEffect(() => {
    if (isStaticData) {
      // 静态数据
      updater.list(q ? filter(allList, ({ label }) => (label || '').toLowerCase().includes(q.toLowerCase())) : allList);
    }
  }, [allList, q, isStaticData]);

  React.useEffect(() => {
    propsOnDropdownVisible?.(visible);
  }, [visible]);

  React.useEffect(() => {
    if (!isStaticData) {
      if (visible && isEmpty(list) && !firstReq && !isCategoryMode) {
        // 第一次请求,在visible的时候请求,但如果是category模式,会自动changeCategory触发
        updater.firstReq(true);
        getList({ pageNo: 1 });
      }
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [isStaticData, list, visible, firstReq]);

  useDeepCompareEffect(() => {
    // 外部查询条件改变,重置state,在visible改变的时候,重新出发上面的请求
    if (!isEmpty(extraQuery)) {
      update({
        firstReq: false,
        list: [],
        hasMore: false,
        chosenCategory: undefined,
        q: undefined,
        pageNo: 1,
      });
    }
  }, [extraQuery]);

  const getList = (params?: any) => {
    const query = { pageSize, pageNo: 1, q, category: chosenCategory, ...extraQuery, ...params };
    if (query.q) query.category = undefined; // 类查询和关键字查询对立
    updater.loading(true);
    const res = getData && getData(query);
    if (res && isPromise(res)) {
      res.then((resData) => {
        const { total, list: curList } = dataFormatter(resData);
        updater.hasMore(Math.ceil(total / pageSize) > query.pageNo);
        let newList = [];
        if (query.pageNo === 1) {
          newList = [...curList];
        } else {
          newList = [...list, ...curList];
        }
        updater.list(newList);
        updater.loading(false);
      });
    }
  };

  const changeQuery = (qStr?: string) => {
    update({
      pageNo: 1,
      q: qStr || undefined,
    });
    !isStaticData && visible && getList({ pageNo: 1, q: qStr || undefined });
  };

  const onDropdownVisible = (vis: boolean) => {
    updater.visible(vis);
  };
  const onLoadMore = () => {
    updater.pageNo(pageNo + 1);
    getList({ pageNo: pageNo + 1, category: chosenCategory });
  };

  const onChangeCategory = (category: string) => {
    if (isCategoryMode && category) {
      changeCategory && changeCategory(category);
      updater.chosenCategory(category);
      update({
        q: undefined,
        pageNo: 1,
        chosenCategory: category,
      });
      getList({ pageNo: 1, q: undefined, category });
    }
  };

  const LoadMoreComp = (LoadMoreRender || DefaultLoadMoreRender) as React.ReactType;

  const LoadMore = hasMore ? <LoadMoreComp onLoadMore={onLoadMore} loading={loading} /> : null;

  return (
    <PureLoadMoreSelector
      onDropdownVisible={onDropdownVisible}
      list={list}
      type={type}
      loading={loading}
      LoadMoreRender={LoadMore}
      changeQuery={changeQuery}
      onChangeCategory={onChangeCategory}
      q={q}
      {...rest}
    />
  );
}
Example #3
Source File: use-hooks.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
/**
 * Filter功能的包装hook, 可以实现自动管理分页跳转、查询条件贮存url,当页面中仅有一个Filter时使用此hook
 * @param getData 必传 用于取列表数据的effect
 * @param fieldConvertor 选传 用于对特殊自定义类型的域的值进行转换
 * @param pageSize 选传 不传默认为10
 * @param extraQuery 选传 查询时需要加入但不存在Filter组件中的参数 注意!!! extraQuery不能直接依赖于路由参数本身,如果必须依赖那就建立一个本地state来替代。否则在前进后退中会额外触发路由变化引起崩溃。另外extraQuery不要和searchQuery混在一起,更新searchQuery用onSubmit, 更新extraQuery直接更新对象, extraQuery的优先级大于searchQuery
 * @param excludeQuery 选传 在映射url时,将查询条件以外的query保留
 * @param fullRange 选传 date类型是否无视时间(仅日期) 时间从前一天的0点到后一天的23:59:59
 * @param dateFormat 选传 日期类型域的toString格式
 * @param requiredKeys 选传 当requiredKeys中的任何参数为空时,终止search行为,当下一次参数有值了才查询。用于页面初始化时某key参数需要异步拿到,不能直接查询的场景
 * @param loadMore 选传 当列表是使用loadMore组件加载时,不在url更新pageNo, LoadMore组件需要关闭initialLoad
 * @param debounceGap 选传 当需要在输入时搜索,加入debounce的时间间隔,不传默认为0
 * @param localMode 选传 为true时,与url切断联系,状态只保存在state中
 * @param lazy 选传 为true时,首次query必须由onSubmit触发
 * @param initQuery 选传 当初次加载页面时filter组件的默认值,如果url上的query不为空,则此值无效
 * @return {queryCondition, onSubmit, onReset, onPageChange, pageNo, fetchDataWithQuery }
 */
export function useFilter<T>(props: ISingleFilterProps<T>): IUseFilterProps<T> {
  const {
    getData,
    excludeQuery = [],
    fieldConvertor,
    extraQuery = {},
    fullRange,
    dateFormat,
    requiredKeys = [],
    loadMore = false,
    debounceGap = 0,
    localMode = false,
    lazy = false,
    initQuery = {},
  } = props;
  const [query, currentRoute] = routeInfoStore.useStore((s) => [s.query, s.currentRoute]);
  const { pageNo: pNo, ...restQuery } = query;

  const [state, update, updater] = useUpdate({
    searchQuery: localMode
      ? {}
      : isEmpty(restQuery)
      ? initQuery
      : !isEmpty(excludeQuery)
      ? omit(restQuery, excludeQuery)
      : { ...initQuery, ...restQuery },
    pageNo: Number(localMode ? 1 : pNo || 1),
    loaded: false,
    pageSize: PAGINATION.pageSize,
    currentPath: currentRoute.path,
  });

  const { searchQuery, pageNo, pageSize, loaded, currentPath } = state;

  const fetchData = React.useCallback(
    debounce((q: any) => {
      const { [FilterBarHandle.filterDataKey]: _Q_, ...restQ } = q;
      getData({ ...restQ });
    }, debounceGap),
    [getData],
  );

  const updateSearchQuery = React.useCallback(() => {
    // 这里异步处理一把是为了不出现dva报错。dva移除后可以考虑放开
    setTimeout(() => {
      setSearch({ ...searchQuery, ...extraQuery, pageNo: loadMore ? undefined : pageNo }, excludeQuery, true);
    });
  }, [excludeQuery, extraQuery, loadMore, pageNo, searchQuery]);

  useDeepCompareEffect(() => {
    const payload = { pageSize, ...searchQuery, ...extraQuery, pageNo };
    const unableToSearch = some(requiredKeys, (key) => payload[key] === '' || payload[key] === undefined);
    if (unableToSearch || (lazy && !loaded)) {
      return;
    }
    pageNo && fetchData(payload);
    updateSearchQuery();
  }, [pageNo, pageSize, searchQuery, extraQuery]);

  // useUpdateEffect 是当mounted之后才会触发的effect
  // 这里的目的是当用户点击当前页面的菜单路由时,url的query会被清空。此时需要reset整个filter
  useUpdateEffect(() => {
    // 当点击菜单时,href会把query覆盖,此时判断query是否为空并且路径没有改变的情况下重新初始化query
    if (isEmpty(query) && currentPath === currentRoute.path) {
      onReset();
      updateSearchQuery();
    }
  }, [query, currentPath, currentRoute]);

  const fetchDataWithQuery = (pageNum?: number) => {
    if (pageNum && pageNum !== pageNo && !loadMore) {
      update.pageNo(pageNum);
    } else {
      const { [FilterBarHandle.filterDataKey]: _Q_, ...restSearchQuery } = searchQuery;
      return getData({
        pageSize,
        ...extraQuery,
        ...restSearchQuery,
        pageNo: pageNum || pageNo,
      });
    }
  };

  const onSubmit = (condition: { [prop: string]: any }) => {
    const formatCondition = convertFilterParamsToUrlFormat(fullRange, dateFormat)(condition, fieldConvertor);
    if (isEqual(formatCondition, searchQuery)) {
      // 如果查询条件没有变化,重复点击查询,还是要强制刷新
      fetchDataWithQuery(1);
    } else {
      update.searchQuery({ ...formatCondition, pageNo: 1 }); // 参数变化时始终重置pageNo
      update.pageNo(1);
    }
    update.loaded(true);
  };

  const onReset = () => {
    if (isEmpty(searchQuery)) {
      fetchDataWithQuery(1);
    } else {
      // reset之后理论上值要变回最开始?
      update.searchQuery(
        localMode
          ? {}
          : isEmpty(restQuery)
          ? initQuery
          : !isEmpty(excludeQuery)
          ? omit(restQuery, excludeQuery)
          : restQuery,
      );
      update.pageNo(1);
    }
  };

  const onPageChange = (currentPageNo: number, currentPageSize?: number) => {
    updater({
      pageNo: currentPageNo,
      pageSize: currentPageSize,
    });
  };

  const onTableChange = (pagination: any, _filters: any, sorter: Obj<any>) => {
    if (!isEmpty(sorter)) {
      const { field, order } = sorter;
      if (order) {
        update.searchQuery({ ...searchQuery, orderBy: field, asc: order === 'ascend' });
      } else {
        update.searchQuery({ ...searchQuery, orderBy: undefined, asc: undefined });
      }
    }
    if (!isEmpty(pagination)) {
      const { pageSize: pSize, current } = pagination;
      update.searchQuery({ ...searchQuery, pageSize: pSize, pageNo: current });
      update.pageNo(current || 1);
    }
  };

  const sizeChangePagination = (paging: IPaging) => {
    const { pageSize: pSize, total } = paging;
    let sizeOptions = PAGINATION.pageSizeOptions;
    if (!sizeOptions.includes(`${pageSize}`)) {
      // 备选项中不存在默认的页数
      sizeOptions.push(`${pageSize}`);
    }
    sizeOptions = sortBy(sizeOptions, (item) => +item);
    return (
      <div className="mt-4 flex items-center flex-wrap justify-end">
        <Pagination
          current={pageNo}
          pageSize={+pSize}
          total={total}
          onChange={onPageChange}
          showSizeChanger
          pageSizeOptions={sizeOptions}
        />
      </div>
    );
  };
  return {
    queryCondition: searchQuery,
    onSubmit, // 包装原始onSubmit, 当搜索时自动更新url
    onReset, // 包装原始onReset, 当重置时自动更新url
    onPageChange, // 当Table切换页码时记录PageNo并发起请求
    pageNo, // 返回当前pageNo,与paging的PageNo理论上相同
    fetchDataWithQuery, // 当页面表格发生操作(删除,启动,编辑)后,进行刷新页面,如不指定pageNum则使用当前页码
    autoPagination: (paging: IPaging) => ({
      total: paging.total,
      current: paging.pageNo,
      pageSize: paging.pageSize,
      // hideOnSinglePage: true,
      onChange: (currentPageNo: number, currentPageSize: number) => onPageChange(currentPageNo, currentPageSize),
    }),
    onTableChange, // Filter/Sort/Paging变化
    sizeChangePagination,
  };
}
Example #4
Source File: use-hooks.tsx    From erda-ui with GNU Affero General Public License v3.0 4 votes vote down vote up
useMultiFilter = (props: IMultiModeProps): IUseMultiFilterProps => {
  const {
    getData,
    excludeQuery = [],
    fieldConvertor,
    pageSize = PAGINATION.pageSize,
    multiGroupEnums,
    groupKey = 'type',
    extraQueryFunc = () => ({}),
    fullRange,
    dateFormat,
    shareQuery = false,
    activeKeyInParam = false,
    requiredKeys = [],
    checkParams = [],
  } = props;
  const wholeExcludeKeys = activeKeyInParam ? excludeQuery : excludeQuery.concat([groupKey]);
  const [query, params, currentRoute] = routeInfoStore.useStore((s) => [s.query, s.params, s.currentRoute]);
  const { pageNo: pNo, ...restQuery } = query;

  const pickQueryValue = React.useCallback(() => {
    return omit(restQuery, wholeExcludeKeys) || {};
  }, [restQuery, wholeExcludeKeys]);

  const activeType = (activeKeyInParam ? params[groupKey] : query[groupKey]) || multiGroupEnums[0];

  const [state, update] = useUpdate({
    groupSearchQuery: multiGroupEnums.reduce((acc, item) => {
      acc[item] = item === activeType || shareQuery ? pickQueryValue() : {};
      return acc;
    }, {}),
    groupPageNo: multiGroupEnums.reduce((acc, item) => {
      acc[item] = item === activeType ? Number(pNo || 1) : 1;
      return acc;
    }, {}),
    activeGroup: activeType,
    currentPath: currentRoute.path,
  });

  const { groupSearchQuery, groupPageNo, activeGroup, currentPath } = state;
  const extraQuery = extraQueryFunc(activeGroup);

  const pageNo = React.useMemo(() => {
    if (activeGroup) {
      return groupPageNo[activeGroup];
    }
    return 1;
  }, [activeGroup, groupPageNo]);

  useDeepCompareEffect(() => {
    // 因为multiGroupEnums随着渲染一直变化引用,所以使用useDeepCompareEffect
    if (activeType !== activeGroup) {
      update.activeGroup(activeType);
    }
  }, [multiGroupEnums, groupKey, activeKeyInParam, params, query, update]);
  useUpdateEffect(() => {
    // 当点击菜单时,href会把query覆盖,此时判断query是否为空并且路径没有改变的情况下重新初始化query
    if (isEmpty(query) && currentPath === currentRoute.path) {
      onReset();
      updateSearchQuery();
    }
  }, [query, currentPath, currentRoute]);

  const currentFetchEffect = getData.length === 1 ? getData[0] : getData[multiGroupEnums.indexOf(activeGroup)];

  const searchQuery = React.useMemo(() => {
    if (activeGroup) {
      return groupSearchQuery[activeGroup];
    }
    return {};
  }, [groupSearchQuery, activeGroup]);

  const updateSearchQuery = React.useCallback(() => {
    setTimeout(() => {
      setSearch({ ...searchQuery, pageNo }, wholeExcludeKeys, true);
    }, 0);
  }, [searchQuery, pageNo, wholeExcludeKeys]);

  const fetchData = (pageNum?: number) => {
    if (checkParams.length) {
      const checked = checkParams.every((key) => !isEmpty(extraQuery[key]));
      if (!checked) {
        return;
      }
    }
    currentFetchEffect({
      pageSize,
      ...extraQuery,
      ...searchQuery,
      pageNo: pageNum || pageNo,
    });
  };

  useDeepCompareEffect(() => {
    const payload = { pageSize, ...extraQuery, ...searchQuery, pageNo };
    const unableToSearch = some(requiredKeys, (key) => payload[key] === '' || payload[key] === undefined);
    if (unableToSearch) {
      return;
    }
    fetchData();
    updateSearchQuery();
  }, [pageNo, pageSize, searchQuery, extraQuery]);

  const fetchDataWithQuery = (pageNum?: number) => {
    if (pageNum && pageNum !== pageNo) {
      onPageChange(pageNum);
    } else {
      fetchData(pageNum);
    }
  };

  const onSubmit = (condition: { [prop: string]: any }) => {
    const formatCondition = convertFilterParamsToUrlFormat(fullRange, dateFormat)(condition, fieldConvertor);
    if (isEqual(formatCondition, searchQuery)) {
      // 如果查询条件没有变化,重复点击查询,还是要强制刷新
      fetchDataWithQuery(1);
    } else if (shareQuery) {
      update.groupSearchQuery(mapValues(groupSearchQuery, () => formatCondition));
      update.groupPageNo(mapValues(groupPageNo, () => 1));
    } else {
      update.groupSearchQuery({
        ...groupSearchQuery,
        [activeGroup]: formatCondition,
      });
      update.groupPageNo({ ...groupPageNo, [activeGroup]: 1 });
    }
  };

  const onReset = () => {
    if (isEmpty(searchQuery)) {
      fetchDataWithQuery(1);
    } else {
      update.groupSearchQuery({ ...groupSearchQuery, [activeGroup]: {} });
      update.groupPageNo({ ...groupPageNo, [activeGroup]: 1 });
    }
  };

  const onPageChange = (currentPageNo: number) => {
    update.groupPageNo({ ...groupPageNo, [activeGroup]: currentPageNo });
  };

  return {
    queryCondition: searchQuery,
    onSubmit, // 包装原始onSubmit, 当搜索时自动更新url
    onReset, // 包装原始onReset, 当重置时自动更新url
    onPageChange, // 当Table切换页码时记录PageNo并发起请求
    pageNo, // 返回当前pageNo,与paging的PageNo理论上相同
    fetchDataWithQuery, // 当页面表格发生操作(删除,启动,编辑)后,进行刷新页面,如不指定pageNum则使用当前页码
    activeType: activeGroup,
    onChangeType: (t: string | number) => setSearch({ [groupKey || 'type']: t }, wholeExcludeKeys, true),
    autoPagination: (paging: IPaging) => ({
      total: paging.total,
      current: paging.pageNo,
      // hideOnSinglePage: true,
      onChange: (n: number) => onPageChange(n),
    }),
  };
}
Example #5
Source File: PageTabs.tsx    From logseq-plugin-tabs with MIT License 4 votes vote down vote up
export function PageTabs(): JSX.Element {
  const [tabs, setTabs] = useStoreTabs();
  const [activeTab, setActiveTab] = useActiveTab(tabs);

  const currActiveTabRef = React.useRef<ITabInfo | null>();
  const latestTabsRef = useLatest(tabs);

  const onCloseTab = useEventCallback((tab: ITabInfo, force?: boolean) => {
    const idx = tabs.findIndex((t) => isTabEqual(t, tab));

    // Do not close pinned
    if (tabs[idx]?.pinned && !force) {
      return;
    }
    const newTabs = [...tabs];
    newTabs.splice(idx, 1);
    setTabs(newTabs);

    if (newTabs.length === 0) {
      logseq.App.pushState("home");
    } else if (isTabEqual(tab, activeTab)) {
      const newTab = newTabs[Math.min(newTabs.length - 1, idx)];
      setActiveTab(newTab);
    }
  });

  const getCurrentActiveIndex = () => {
    return tabs.findIndex((ct) => isTabEqual(ct, currActiveTabRef.current));
  };

  const onCloseAllTabs = useEventCallback((excludeActive: boolean) => {
    const newTabs = tabs.filter(
      (t) =>
        t.pinned || (excludeActive && isTabEqual(t, currActiveTabRef.current))
    );
    setTabs(newTabs);
    if (!excludeActive) {
      logseq.App.pushState("home");
    }
  });

  const onChangeTab = useEventCallback(async (t: ITabInfo) => {
    setActiveTab(t);
    const idx = getCurrentActiveIndex();
    // remember current page's scroll position
    if (idx !== -1) {
      const scrollTop =
        top?.document.querySelector("#main-container")?.scrollTop;

      setTabs(
        produce(tabs, (draft) => {
          draft[idx].scrollTop = scrollTop;
        })
      );
    }
  });

  const onNewTab = useEventCallback((t: ITabInfo | null, open = false) => {
    if (t) {
      const previous = tabs.find((_t) => isTabEqual(t, _t));
      if (!previous) {
        setTabs([...tabs, t]);
      } else {
        open = true;
      }
      if (open) {
        onChangeTab({ ...t, pinned: previous?.pinned });
      }
    }
  });

  useCaptureAddTabAction(onNewTab);
  useDeepCompareEffect(() => {
    let timer = 0;
    let newTabs = latestTabsRef.current;
    const prevTab = currActiveTabRef.current;
    // If a new ActiveTab is set, we will need to replace or insert the tab
    if (activeTab) {
      newTabs = produce(tabs, (draft) => {
        if (tabs.every((t) => !isTabEqual(t, activeTab))) {
          const currentIndex = draft.findIndex((t) => isTabEqual(t, prevTab));
          const currentPinned = draft[currentIndex]?.pinned;
          if (currentIndex === -1 || currentPinned) {
            draft.push(activeTab);
          } else {
            draft[currentIndex] = activeTab;
          }
        } else {
          // Update the data if it is already in the list (to update icons etc)
          const currentIndex = draft.findIndex((t) => isTabEqual(t, activeTab));
          draft[currentIndex] = activeTab;
        }
      });
      timer = setTimeout(async () => {
        const p = await logseq.Editor.getCurrentPage();
        if (!isTabEqual(activeTab, p)) {
          logseq.App.pushState("page", {
            name: isBlock(activeTab)
              ? activeTab.uuid
              : activeTab.originalName ?? activeTab.name,
          });
        }
      }, 200);
    }
    currActiveTabRef.current = activeTab;
    setTabs(newTabs);
    return () => {
      if (timer) {
        clearTimeout(timer);
      }
    };
  }, [activeTab ?? {}]);

  const onPinTab = useEventCallback((t) => {
    setTabs(
      produce(tabs, (draft) => {
        const idx = draft.findIndex((ct) => isTabEqual(ct, t));
        draft[idx].pinned = !draft[idx].pinned;
        sortTabs(draft);
      })
    );
  });

  const onSwapTab = (t0: ITabInfo, t1: ITabInfo) => {
    setTabs(
      produce(tabs, (draft) => {
        const i0 = draft.findIndex((t) => isTabEqual(t, t0));
        const i1 = draft.findIndex((t) => isTabEqual(t, t1));
        draft[i0] = t1;
        draft[i1] = t0;
        sortTabs(draft);
      })
    );
  };

  const ref = React.useRef<HTMLElement>(null);
  const scrollWidth = useScrollWidth(ref);

  useAdaptMainUIStyle(tabs.length > 0, scrollWidth);

  React.useEffect(() => {
    if (activeTab && ref) {
      setTimeout(() => {
        ref.current
          ?.querySelector(`[data-active="true"]`)
          ?.scrollIntoView({ behavior: "smooth" });
      }, 100);
    }
  }, [activeTab, ref]);

  useRegisterKeybindings("tabs:toggle-pin", () => {
    if (currActiveTabRef.current) {
      onPinTab(currActiveTabRef.current);
    }
  });

  useRegisterKeybindings("tabs:close", () => {
    if (currActiveTabRef.current) {
      onCloseTab(currActiveTabRef.current);
    }
  });

  useRegisterKeybindings("tabs:select-next", () => {
    let idx = getCurrentActiveIndex() ?? -1;
    idx = (idx + 1) % tabs.length;
    onChangeTab(tabs[idx]);
  });

  useRegisterKeybindings("tabs:select-prev", () => {
    let idx = getCurrentActiveIndex() ?? -1;
    idx = (idx - 1 + tabs.length) % tabs.length;
    onChangeTab(tabs[idx]);
  });

  useRegisterSelectNthTabKeybindings((idx) => {
    if (idx > 0 && idx <= tabs.length) {
      onChangeTab(tabs[idx - 1]);
    }
  });

  useRegisterCloseAllButPins(onCloseAllTabs);

  return (
    <Tabs
      ref={ref}
      onClickTab={onChangeTab}
      activeTab={activeTab}
      tabs={tabs}
      onSwapTab={onSwapTab}
      onPinTab={onPinTab}
      onCloseTab={onCloseTab}
      onCloseAllTabs={onCloseAllTabs}
    />
  );
}