feat: manually mirror opencoze's code from bytedance
Change-Id: I09a73aadda978ad9511264a756b2ce51f5761adf
This commit is contained in:
22
frontend/packages/arch/hooks/src/index.ts
Normal file
22
frontend/packages/arch/hooks/src/index.ts
Normal 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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
45
frontend/packages/arch/hooks/src/use-boolean/index.ts
Normal file
45
frontend/packages/arch/hooks/src/use-boolean/index.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
61
frontend/packages/arch/hooks/src/use-hover/index.ts
Normal file
61
frontend/packages/arch/hooks/src/use-hover/index.ts
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
46
frontend/packages/arch/hooks/src/use-state-realtime/index.ts
Normal file
46
frontend/packages/arch/hooks/src/use-state-realtime/index.ts
Normal 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;
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
62
frontend/packages/arch/hooks/src/use-toggle/index.ts
Normal file
62
frontend/packages/arch/hooks/src/use-toggle/index.ts
Normal 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
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
32
frontend/packages/arch/hooks/src/use-update-effect/index.ts
Normal file
32
frontend/packages/arch/hooks/src/use-update-effect/index.ts
Normal 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;
|
||||
262
frontend/packages/arch/hooks/src/use-url-params/index.ts
Normal file
262
frontend/packages/arch/hooks/src/use-url-params/index.ts
Normal 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;
|
||||
Reference in New Issue
Block a user