feat: manually mirror opencoze's code from bytedance

Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
fanlv
2025-07-20 17:36:12 +08:00
commit 890153324f
14811 changed files with 1923430 additions and 0 deletions

View File

@@ -0,0 +1,22 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { default as useHover } from './use-hover';
export { default as usePersistCallback } from './use-persist-callback';
export { default as useUpdateEffect } from './use-update-effect';
export { default as useToggle } from './use-toggle';
export { default as useUrlParams } from './use-url-params';
export { default as useStateRealtime } from './use-state-realtime';

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook, act } from '@testing-library/react-hooks';
import useBoolean from '../index';
describe('useBoolean', () => {
it('uses methods', () => {
const hook = renderHook(() => useBoolean());
expect(hook.result.current.state).toBeFalsy();
act(() => {
hook.result.current.setFalse();
});
expect(hook.result.current.state).toBeFalsy();
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBeTruthy();
act(() => {
hook.result.current.setTrue();
});
expect(hook.result.current.state).toBeTruthy();
act(() => {
hook.result.current.toggle(true);
});
expect(hook.result.current.state).toBeTruthy();
});
it('uses defaultValue', () => {
const hook = renderHook(() => useBoolean(true));
expect(hook.result.current.state).toBeTruthy();
});
});

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useMemo } from 'react';
export interface ReturnValue {
state: boolean;
setTrue: () => void;
setFalse: () => void;
toggle: (value?: boolean) => void;
}
export default (initialValue?: boolean): ReturnValue => {
const [state, setState] = useState(Boolean(initialValue));
const stateMethods = useMemo(() => {
const setTrue = () => setState(true);
const setFalse = () => setState(false);
const toggle = (val?: boolean) =>
setState(typeof val === 'boolean' ? val : s => !s);
return {
setTrue,
setFalse,
toggle,
};
}, []);
return {
state,
...stateMethods,
};
};

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import useHover from '../index';
describe('useHover', () => {
it('test div element ref', () => {
const target = document.createElement('div');
const handleEnter = vi.fn();
const handleLeave = vi.fn();
const hook = renderHook(() =>
useHover(target, {
onEnter: handleEnter,
onLeave: handleLeave,
}),
);
expect(hook.result.current[1]).toBe(false);
act(() => {
target.dispatchEvent(new Event('mouseenter'));
});
expect(hook.result.current[1]).toBe(true);
expect(handleEnter).toBeCalledTimes(1);
act(() => {
target.dispatchEvent(new Event('mouseleave'));
});
expect(hook.result.current[1]).toBe(false);
expect(handleLeave).toBeCalledTimes(1);
hook.unmount();
});
});

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {type DependencyList} from 'react';
import type React from 'react';
import { useState, useCallback, useRef, useLayoutEffect } from 'react';
interface Options {
onEnter?: () => void
onLeave?: () => void
}
const useHover = <T extends HTMLElement = any>(
el?: T | (() => T),
options: Options = {},
deps: DependencyList = [],
): [React.MutableRefObject<T>, boolean] => {
const { onEnter, onLeave } = options
const ref = useRef<T>();
const [isHovered, setIsHovered] = useState(false);
const handleMouseEnter = useCallback(() => {
if (onEnter) {onEnter()}
setIsHovered(true)
}, [typeof onEnter === 'function']);
const handleMouseLeave = useCallback(() => {
if (onLeave) {onLeave()}
setIsHovered(false)
}, [typeof onLeave === 'function']);
useLayoutEffect(() => {
let target = ref.current
if (el) {
target = typeof el === 'function' ? el() : el;
}
if (!target) {return}
target.addEventListener('mouseenter', handleMouseEnter);
target.addEventListener('mouseleave', handleMouseLeave);
return () => {
target?.removeEventListener('mouseenter', handleMouseEnter);
target?.removeEventListener('mouseleave', handleMouseLeave);
};
}, [ref.current, typeof el === 'function' ? undefined : el, ...deps]);
return [ref as React.MutableRefObject<T>, isHovered];
};
export default useHover;

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, renderHook, type RenderHookResult } from '@testing-library/react-hooks';
import { useState } from 'react';
import usePersistCallback from '..';
// 函数变化,但是地址不变
const TestHooks = () => {
const [count, setCount] = useState(0);
const addCount = () => {
setCount(c => c + 1);
};
const persistFn = usePersistCallback(() => count);
return { addCount, persistFn };
};
let hook: RenderHookResult<[], ReturnType<typeof TestHooks>>;
describe('usePersistCallback', () => {
it('usePersistCallback should work', () => {
act(() => {
hook = renderHook(() => TestHooks());
});
const currentFn = hook.result.current.persistFn;
expect(hook.result.current.persistFn()).toEqual(0);
act(() => {
hook.result.current.addCount();
});
expect(currentFn).toEqual(hook.result.current.persistFn);
expect(hook.result.current.persistFn()).toEqual(1);
});
});

View File

@@ -0,0 +1,33 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useRef, useCallback, useMemo } from 'react';
function usePersistCallback<T extends (...args: any[]) => any>(fn?: T) {
const ref = useRef<T>();
ref.current = useMemo(() => fn, [fn]);
return useCallback<T>(
// @ts-expect-error ignore
(...args) => {
const f = ref.current;
return f && f(...args);
},
[ref],
);
}
export default usePersistCallback;

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable @typescript-eslint/no-unused-vars */
import { act, renderHook } from '@testing-library/react-hooks';
import useStateRealtime from '../index';
describe('useStateRealtime', () => {
it('initState undefined', () => {
const { result } = renderHook(() => useStateRealtime());
const [state, setState, getRealVal] = result.current;
expect(state).toBeUndefined();
});
it('initState number 2', () => {
const { result } = renderHook(() => useStateRealtime(2));
const [state, setState, getRealVal] = result.current;
expect(state).toBe(2);
});
it('initState function 10', () => {
const { result } = renderHook(() => useStateRealtime(() => 10));
const [state, setState, getRealVal] = result.current;
expect(state).toBe(10);
});
it('method setState', () => {
const { result } = renderHook(() => useStateRealtime(1));
const [state, setState, getRealVal] = result.current;
expect(result.current[0]).toBe(1);
act(() => {
setState(2);
});
expect(result.current[0]).toBe(2);
});
it('method setState param function', () => {
const { result } = renderHook(() => useStateRealtime(() => 10));
const [state, setState, getRealVal] = result.current;
expect(result.current[0]).toBe(10);
act(() => {
setState(pre => pre + 2);
});
expect(result.current[0]).toBe(12);
});
it('method getRealVal', () => {
const { result } = renderHook(() => useStateRealtime(1));
const [state, setState, getRealVal] = result.current;
act(() => {
setState(2);
});
expect(getRealVal()).toBe(2);
});
it('method getRealVal function', () => {
const { result } = renderHook(() => useStateRealtime(() => 10));
const [state, setState, getRealVal] = result.current;
act(() => {
setState(pre => pre + 2);
});
expect(getRealVal()).toBe(12);
});
it('test batchUpdate', () => {
const { result } = renderHook(() => useStateRealtime(1));
const [state, setState, getRealVal] = result.current;
act(() => {
setState(pre => pre + 2);
expect(getRealVal()).toBe(3);
setState(pre => pre + 2);
expect(getRealVal()).toBe(5);
});
expect(result.current[0]).toBe(5);
expect(getRealVal()).toBe(5);
act(() => {
setState(pre => pre + 4);
expect(getRealVal()).toBe(9);
setState(pre => pre + 4);
expect(getRealVal()).toBe(13);
});
expect(result.current[0]).toBe(13);
expect(getRealVal()).toBe(13);
});
});

View File

@@ -0,0 +1,46 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useRef, type Dispatch, type SetStateAction, useCallback } from 'react';
const isFunction = (val: any): val is Function => typeof val === 'function';
// 获取新的状态值,兼容传值和传函数情况
function getStateVal<T>(preState: T, initVal?: SetStateAction<T>): T | undefined {
if (isFunction(initVal)) {
return initVal(preState);
}
return initVal;
}
function useStateRealtime<T>(initialState: T | (() => T)): [T, Dispatch<SetStateAction<T>>, () => T]
function useStateRealtime<T = undefined>(): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined]
function useStateRealtime<T>(
initVal?: T | (() => T),
): [T | undefined, Dispatch<SetStateAction<T | undefined>>, () => T | undefined] {
const initState = getStateVal(undefined, initVal);
const [val, setVal] = useState(initState);
const valRef = useRef(initState);
const setState = useCallback((newVal?: SetStateAction<T | undefined>) => {
const newState = getStateVal(valRef.current, newVal);
valRef.current = newState;
setVal(newState);
}, [])
const getRealState = useCallback(() => valRef.current, [])
return [val, setState, getRealState];
}
export default useStateRealtime;

View File

@@ -0,0 +1,116 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { act, renderHook } from '@testing-library/react-hooks';
import useToggle from '../index';
describe('useToggle', () => {
it('toggle values', () => {
const hook = renderHook(() => useToggle());
expect(hook.result.current.state).toBeFalsy();
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBeTruthy();
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBeFalsy();
act(() => {
hook.result.current.toggle(false);
});
expect(hook.result.current.state).toBeFalsy();
act(() => {
hook.result.current.toggle(true);
});
expect(hook.result.current.state).toBeTruthy();
});
it('default value', () => {
const hook = renderHook(() => useToggle(true));
expect(hook.result.current.state).toBeTruthy();
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBeFalsy();
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBeTruthy();
act(() => {
hook.result.current.toggle(true);
});
expect(hook.result.current.state).toBeTruthy();
});
it('default non-boolean value', () => {
const defaultValue = {};
const hook = renderHook(() => useToggle(defaultValue));
expect(hook.result.current.state).toBe(defaultValue);
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBe(false);
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBe(defaultValue);
act(() => {
hook.result.current.toggle(defaultValue);
});
expect(hook.result.current.state).toBe(defaultValue);
});
it('default non-boolean values', () => {
enum Theme {
Light = 0,
Dark,
}
const hook = renderHook(() => useToggle<Theme>(Theme.Light, Theme.Dark));
expect(hook.result.current.state).toBe(Theme.Light);
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBe(Theme.Dark);
act(() => {
hook.result.current.toggle();
});
expect(hook.result.current.state).toBe(Theme.Light);
act(() => {
hook.result.current.toggle(Theme.Light);
});
expect(hook.result.current.state).toBe(Theme.Light);
act(() => {
hook.result.current.toggle(Theme.Dark);
});
expect(hook.result.current.state).toBe(Theme.Dark);
});
});

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useState, useMemo } from 'react'
type State = any
export interface ReturnValue<T = State> {
state: T;
toggle: (value?: T) => void;
}
function useToggle<T = boolean>(): ReturnValue<T>
function useToggle<T = State>(defaultValue: T): ReturnValue<T>
function useToggle<T = State, U = State>(
defaultValue: T,
reverseValue: U,
): ReturnValue<T | U>
function useToggle<D extends State = State, R extends State = State>(
defaultValue: D = false as D,
reverseValue?: R,
) {
const [state, setState] = useState<D | R>(defaultValue)
const actions = useMemo(() => {
const reverseValueOrigin = (reverseValue === undefined ? !defaultValue : reverseValue) as D | R
const toggle = (value?: D | R) => {
if (value !== undefined) {
setState(value)
return
}
setState((s) => (s === defaultValue ? reverseValueOrigin : defaultValue))
}
return {
toggle,
}
}, [defaultValue, reverseValue])
return {
state,
...actions,
}
}
export default useToggle

View File

@@ -0,0 +1,45 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { renderHook } from '@testing-library/react-hooks';
import useUpdateEffect from '../index';
describe('useUpdateEffect', () => {
it('test on mounted', () => {
let mountedState = 1;
const hook = renderHook(() =>
useUpdateEffect(() => {
mountedState = 2;
}),
);
expect(mountedState).toEqual(1);
hook.rerender();
expect(mountedState).toEqual(2);
});
it('test on optional', () => {
let mountedState = 1;
const hook = renderHook(() =>
useUpdateEffect(() => {
mountedState = 3;
}, [mountedState]),
);
expect(mountedState).toEqual(1);
mountedState = 2;
hook.rerender();
expect(mountedState).toEqual(3);
});
});

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { useEffect, useRef } from 'react';
const useUpdateEffect: typeof useEffect = (effect, deps) => {
const isMounted = useRef(false);
useEffect(() => {
if (!isMounted.current) {
isMounted.current = true;
} else {
return effect();
}
return () => {};
}, deps);
};
export default useUpdateEffect;

View File

@@ -0,0 +1,262 @@
/*
* Copyright 2025 coze-dev Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
useEffect,
useState,
type Dispatch,
type SetStateAction,
useMemo,
useCallback,
useRef,
} from 'react';
import queryString, {
type ParseOptions,
type StringifyOptions,
} from 'query-string';
import { omit as _omit } from 'lodash-es';
import useBoolean from '../use-boolean';
export interface ReturnValue<T> {
value: T;
setValue: Dispatch<SetStateAction<T>>;
resetParams: (initial?: boolean) => void;
}
export interface KeyValue {
[key: string]: any;
}
export type KeysObj<T> = {
[key in keyof T]: any;
};
interface AutoMergeUrlParamsOptions {
useUrlParamsOnFirst: boolean;
}
interface IOptions {
omitKeys?: string[]; // 在 url 中不展示的字段但是还是会传到最后的返回的 value 中
autoFormat?: boolean;
autoMergeUrlParamsOptions?: AutoMergeUrlParamsOptions;
autoMergeUrlParams?: boolean;
parseOptions?: ParseOptions;
stringifyOptions?: StringifyOptions;
replaceUrl?: boolean; // Determines if the URL will be replaced or pushed in the browser history
}
const _toString = Object.prototype.toString;
function isObject(val: any) {
return val !== null && typeof val === 'object';
}
function isDate(val: any) {
return _toString.call(val) === '[object Date]';
}
function formatValueFn<T>(obj: T, autoFormat: boolean): KeysObj<T> {
if (autoFormat) {
const formatValue = {} as KeysObj<T>;
for (const key in obj) {
const val = obj[key] as any;
if (val === '' || val === undefined || val === null) {
continue;
}
if (Array.isArray(val)) {
formatValue[key] = val;
} else if (isDate(val)) {
formatValue[key] = val.toISOString();
} else if (isObject(val)) {
formatValue[key] = JSON.stringify(val);
} else {
formatValue[key] = val;
}
}
return formatValue;
} else {
return obj;
}
}
// 第一次初始化是 url merge defaultValue ,然后后续 setValue 用 value merge url
// eslint-disable-next-line max-params
function getMergeValue<T>(
value: T,
parseOptions: ParseOptions,
autoMergeUrlParams: boolean,
isFirstMerged: boolean,
autoMergeUrlParamsOptions: AutoMergeUrlParamsOptions,
) {
let mergeValue = (isObject(value) ? { ...value } : ({} as T)) as Record<
string,
unknown
>;
if (autoMergeUrlParams) {
if (isFirstMerged) {
mergeValue = Object.assign(
mergeValue,
queryString.parse(window.location.search, parseOptions),
);
} else {
mergeValue = Object.assign(
queryString.parse(window.location.search, parseOptions),
mergeValue,
);
}
} else {
if (isFirstMerged && autoMergeUrlParamsOptions?.useUrlParamsOnFirst) {
mergeValue = Object.assign(
mergeValue,
queryString.parse(window.location.search, parseOptions),
);
}
}
return mergeValue as T;
}
// 初始化 initValue 其中的 value 值可能会有 number 类型, 会被在 url 转成 Object 全部转换成 string, 需自行处理下
// The value in the initialization initValue may be a number,
// which will be converted into an Object in the url and all converted into a string, which needs to be processed manually.
function useUrlParams<T>(
initValue: T = {} as T,
options?: IOptions,
): ReturnValue<T> {
const {
omitKeys,
autoFormat,
autoMergeUrlParams,
parseOptions,
stringifyOptions,
replaceUrl,
autoMergeUrlParamsOptions,
} = Object.assign(
{
omitKeys: [],
autoFormat: false,
autoMergeUrlParams: true,
autoMergeUrlParamsOptions: {
useUrlParamsOnFirst: false,
},
parseOptions: { arrayFormat: 'bracket' },
stringifyOptions: {
skipNull: true,
skipEmptyString: true,
arrayFormat: 'bracket',
},
replaceUrl: true,
},
options,
);
const [value, setValue] = useState<T>(
getMergeValue(
initValue,
parseOptions,
autoMergeUrlParams,
true,
autoMergeUrlParamsOptions,
),
);
const {
state: isPopping,
setTrue: setPoppingTrue,
setFalse: setPoppingFalse,
} = useBoolean(false);
const initialValueRef = useRef<T>(value);
const isFirstMerged = useRef<boolean>(true);
const resetParams = useCallback((initial = true) => {
if (initial) {
setValue(initialValueRef.current!);
} else {
setValue(queryString.parse(window.location.search, parseOptions) as any);
}
}, []);
const formatValue = useMemo<KeysObj<T>>(
() => formatValueFn<T>(value, autoFormat),
[value],
);
useEffect(() => {
const fn = () => {
setPoppingTrue();
};
window.addEventListener('popstate', fn);
return () => {
window.removeEventListener('popstate', fn);
};
}, []);
useEffect(() => {
if (isPopping) {
setValue(queryString.parse(window.location.search, parseOptions) as any);
}
}, [isPopping]);
useEffect(() => {
const { href, search, hash } = window.location;
let mergeValue;
if (isFirstMerged.current) {
isFirstMerged.current = false;
mergeValue = formatValue;
} else {
mergeValue = getMergeValue(
formatValue,
parseOptions,
autoMergeUrlParams,
isFirstMerged.current,
autoMergeUrlParamsOptions,
);
}
const searchStr = queryString.stringify(
_omit(mergeValue, omitKeys),
stringifyOptions,
);
const url = `${href.replace(hash, '').replace(search, '')}${
searchStr ? `?${searchStr}` : ''
}${hash}`;
if (replaceUrl) {
window.history.replaceState(
{ ...window.history.state, url, title: document.title },
document.title,
url,
);
} else if (!isPopping) {
window.history.pushState(
{ ...window.history.state, url, title: document.title },
document.title,
url,
);
} else {
// we are popping state, reset to false
setPoppingFalse();
}
}, [formatValue]);
return { value: formatValue, setValue, resetParams };
}
export default useUrlParams;