@wwog/react 1.3.13 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -647,8 +647,19 @@ const result = ruleChecker(registrationData, rules);
647
647
 
648
648
  #### `createExternalState` (v1.2.9+, useGetter added in v1.2.13)
649
649
 
650
+ > v1.3.14: Breaking: `use()` renamed to `useState()` for React 19 compiler compatibility (the compiler requires hook names to start with `use`; the old `use` method was misidentified as a non-hook)
650
651
  > v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
651
652
  > v1.2.13: add useGetter
653
+ > Breaking: `sideEffect` replaced by `onSet` and `onChange` for clearer callback semantics
654
+
655
+ **Migration (v1.3.13 → v1.3.14)**
656
+
657
+ ```diff
658
+ - const [theme, setTheme] = themeState.use();
659
+ + const [theme, setTheme] = themeState.useState();
660
+ ```
661
+
662
+ `useGetter()` is unchanged.
652
663
 
653
664
  > A lightweight external state management utility that allows you to create and manage state outside the React component tree while maintaining perfect integration with components.
654
665
 
@@ -656,12 +667,18 @@ const result = ruleChecker(registrationData, rules);
656
667
 
657
668
  > Extends from createExternalState and uses storage to persist state, supports `localStorage` and `sessionStorage`
658
669
 
670
+ - `createStorageState<T>(key, initialState, options?)`: Creates persisted state
671
+ - `options.onSet`: Invoked on every `set()` (storage write happens first, then the user callback)
672
+ - `options.onChange`: Invoked only when the value actually changes
673
+ - `options.storageType`: `'local'` | `'session'`, defaults to `'local'`
674
+ - `options.transform`: Same as `createExternalState`
675
+
659
676
  ```tsx
660
677
  import { createExternalState } from "@wwog/react";
661
678
 
662
679
  // Create a global theme state
663
680
  const themeState = createExternalState("light", {
664
- sideEffect: (newTheme, oldTheme) => {
681
+ onChange: (newTheme, oldTheme) => {
665
682
  console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
666
683
  },
667
684
  });
@@ -672,7 +689,7 @@ themeState.set("dark");
672
689
 
673
690
  // Use the state in components
674
691
  function ThemeConsumer() {
675
- const [theme, setTheme] = themeState.use();
692
+ const [theme, setTheme] = themeState.useState();
676
693
 
677
694
  return (
678
695
  <div className={theme}>
@@ -695,14 +712,17 @@ function ReadOnlyThemeConsumer() {
695
712
  - `createExternalState<T>(initialState, options?)`: Creates a state accessible outside components
696
713
 
697
714
  - `initialState`: Initial state value
698
- - `options.sideEffect`: Optional side effect function, called on state updates
715
+ - `options.onSet`: Optional callback invoked on every `set()` call, even when the value is unchanged
716
+ - `options.onChange`: Optional callback invoked only when the stored value actually changes (compared via `Object.is`)
717
+ - Both callbacks receive `(newState, prevState)` as the raw internal values (type `T`, before `transform.get`)
699
718
  - Returns an object with methods:
700
719
  - `get()`: Get the current state value
701
720
  - `set(newState)`: Update the state value
702
- - `use()`: React Hook, returns `[state, setState]` for using this state in components
721
+ - `useState()`: React Hook, returns `[state, setState]` for using this state in components (same return shape as React `useState`)
703
722
  - `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
704
723
  - `options.transform`: - `get` - `set`
705
- Use cases:
724
+
725
+ Use cases:
706
726
 
707
727
  - Global state management (themes, user settings, etc.)
708
728
  - Cross-component communication
package/dist/index.d.mts CHANGED
@@ -638,13 +638,13 @@ interface UseControlledOptions<T> {
638
638
  declare function useControlled<T>(options: UseControlledOptions<T>): [T, Dispatch<React.SetStateAction<T>>];
639
639
 
640
640
  /**
641
- * @zh 如果需要在变更状态时执行副作用,可以传入函数,对于异步函数,会在更改状态后执行,不会阻塞状态更新, 尽可能在外部使用useEffect处理异步副作用
642
- * @en If you need to perform side effects when changing the state, you can pass a function. For asynchronous functions, it will be executed after the state changes without blocking the state update, so it's best to use useEffect for handling asynchronous side effects.
641
+ * @zh 状态回调函数。对于异步函数,会在状态更新后执行,不会阻塞状态更新,尽可能在外部使用 useEffect 处理异步副作用。
642
+ * @en State callback function. Async callbacks run after the state update without blocking it; prefer useEffect for async side effects.
643
643
  * @template T The type of the state / 状态的类型
644
644
  * @param newState The new state value / 新的状态值
645
645
  * @param prevState The previous state value / 之前的状态值
646
646
  */
647
- type ExternalSideEffect<T> = (newState: T, prevState: T) => any | Promise<any>;
647
+ type ExternalStateCallback<T> = (newState: T, prevState: T) => any | Promise<any>;
648
648
  /**
649
649
  * @en Transform functions for getting and setting state
650
650
  * @zh 用于获取和设置状态的转换函数
@@ -671,10 +671,15 @@ interface Transform<T, U = T> {
671
671
  */
672
672
  interface ExternalStateOptions<T, U = T> {
673
673
  /**
674
- * @en Side effect function to run after state changes
675
- * @zh 状态变更后运行的副作用函数
674
+ * @en Callback invoked on every `set` call, even when the value is unchanged
675
+ * @zh 每次调用 `set` 后触发,即使值未发生变化
676
676
  */
677
- sideEffect?: ExternalSideEffect<T>;
677
+ onSet?: ExternalStateCallback<T>;
678
+ /**
679
+ * @en Callback invoked only when the stored value actually changes
680
+ * @zh 仅在内部存储值发生变化时触发
681
+ */
682
+ onChange?: ExternalStateCallback<T>;
678
683
  /**
679
684
  * @en Transform functions for getting and setting state
680
685
  * @zh 用于获取和设置状态的转换函数
@@ -703,9 +708,17 @@ interface ExternalState<T, U = T> {
703
708
  /**
704
709
  * @en React Hook for using external state in components
705
710
  * @zh 在组件中使用外部状态的 React Hook
706
- * @returns Array containing current钣金龙8国际唯一官网 current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
711
+ * @deprecated Use `useState()` instead. The method name `use` may not be recognized as a Hook by React Compiler.
712
+ * @zh_CN 请改用 `useState()`。`use` 这个方法名可能无法被 React Compiler 识别为 Hook。
713
+ * @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
707
714
  */
708
715
  use: () => [U, (newState: U | ((prevState: U) => U)) => void];
716
+ /**
717
+ * @en React Hook for using external state in components.
718
+ * @zh 在组件中使用外部状态的 React Hook。
719
+ * @returns Array containing current state and update function, similar to React useState / 包含当前状态和更新函数的数组,类似于 React useState
720
+ */
721
+ useState: () => [U, (newState: U | ((prevState: U) => U)) => void];
709
722
  /**
710
723
  * @zh use的变体,只获取value.
711
724
  * @en A variant of use that only gets the value.
@@ -721,7 +734,7 @@ interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
721
734
  * ```tsx
722
735
  * // Create an app-level theme state with options
723
736
  * const themeState = createExternalState('light', {
724
- * sideEffect: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
737
+ * onChange: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
725
738
  * transform: {
726
739
  * get: (state) => state.toUpperCase(),
727
740
  * set: (value) => value.toLowerCase()
@@ -734,7 +747,7 @@ interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
734
747
  *
735
748
  * // Use state in components
736
749
  * function ThemeConsumer() {
737
- * const [theme, setTheme] = themeState.use();
750
+ * const [theme, setTheme] = themeState.useState();
738
751
  *
739
752
  * return (
740
753
  * <div className={theme}>
@@ -748,7 +761,8 @@ interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
748
761
  */
749
762
  declare function createExternalState<T, U = T>(initialState: T | (() => T), options?: ExternalStateOptions<T, U>): ExternalState<T, U>;
750
763
  interface StorageStateOptions<T, U> {
751
- sideEffect?: (newState: T) => void;
764
+ onSet?: ExternalStateCallback<T>;
765
+ onChange?: ExternalStateCallback<T>;
752
766
  transform?: Transform<T, U>;
753
767
  storageType: 'local' | 'session';
754
768
  }
@@ -866,4 +880,4 @@ declare function getCurrentBreakpoint(breakpointDesc: BreakpointDesc, width: num
866
880
  declare function useScreen(breakpointDesc?: BreakpointDesc): "base" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
867
881
 
868
882
  export { ArrayRender, Boundary, Counter, DateRender, DefBreakpointDesc, False, If, Observer, Pipe, Portal, Repeat, Scope, SizeBox, Styles, Switch, Toggle, True, When, breakpoints, childrenLoop, createExternalState, createStorageState, cx, formatDate, getCurrentBreakpoint, ruleChecker, safePromiseTry, safePromiseWithResolvers, useControlled, useScreen };
869
- export type { ApplyRules, ArrayRenderProps, ArrayRule, ArraySpecificProps, BaseRule, BooleanRule, BoundaryProps, BreakpointDesc, BreakpointName, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, FieldRule, IfProps, LengthRuleProps, NumberRangeProps, NumberRule, ObserverProps, PipeProps, PortalProps, RepeatProps, Responsive, RuleDescription, ScopeProps, StorageStateOptions, StringRule, StringSpecificProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
883
+ export type { ApplyRules, ArrayRenderProps, ArrayRule, ArraySpecificProps, BaseRule, BooleanRule, BoundaryProps, BreakpointDesc, BreakpointName, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalState, ExternalStateCallback, ExternalStateOptions, ExternalWithKernel, FalseProps, FieldRule, IfProps, LengthRuleProps, NumberRangeProps, NumberRule, ObserverProps, PipeProps, PortalProps, RepeatProps, Responsive, RuleDescription, ScopeProps, StorageStateOptions, StringRule, StringSpecificProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import i,{useMemo as S,Fragment as v,Children as q,isValidElement as U,cloneElement as G,useEffect as b,useState as A,useRef as N,Component as K,useCallback as Q,useSyncExternalStore as X}from"react";import{createPortal as ee}from"react-dom";function j(e,r){if(e===void 0)return;let t=0;if(Array.isArray(e)){for(const o of e)if(r(o,t++)===!1)break}else r(e,t)}const te=(e,r)=>e===r,x=e=>i.createElement(i.Fragment,null,e.children);x.displayName="Switch_Case";const C=e=>i.createElement(i.Fragment,null,e.children);C.displayName="Switch_Default";const F=e=>{const{value:r,compare:t=te,children:o,strict:n=!1}=e,l=new Set;let s=null,c=null,a=!1;return j(o,(u,f)=>{if(!i.isValidElement(u))throw new Error(`Switch Children only accepts valid React elements at index ${f}`);const d=u.type;if(d.displayName===x.displayName){const m=u.props;if(l.has(m.value))throw new Error(`Switch found duplicate Case value at index ${f}: ${JSON.stringify(m.value)}${n?" (detected in strict mode)":""}`);if(l.add(m.value),!s&&t(r,m.value)&&(s=m.children,n===!1))return!1}else if(d.displayName===C.displayName){if(a)throw new Error(`Switch can only have one Default child at index ${f}`);if(a=!0,c=u.props.children,!n&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(d.displayName||d.name||d)} at index ${f}`)}),i.createElement(i.Fragment,null,s??c)};F.displayName="Switch",F.Case=x,F.Default=C,F.createTyped=function(){return{Switch:F,Case:x,Default:C}};const M=e=>i.createElement(i.Fragment,null,e.children),I=({children:e})=>i.createElement(i.Fragment,null,e),$=e=>i.createElement(i.Fragment,null,e.children);M.displayName="If_Then",I.displayName="If_Else",$.displayName="If_ElseIf";const w=({condition:e,children:r})=>{let t=null,o=null;const n=[];if(i.Children.forEach(r,l=>{if(!i.isValidElement(l))throw new Error("If component only accepts valid React elements");const s=l.type;if(s.displayName===M.displayName){if(t)throw new Error("If component can only have one Then child");t=l}else if(s.displayName===$.displayName)n.push(l);else if(s.displayName===I.displayName){if(o)throw new Error("If component can only have one Else child");o=l}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),e)return t?i.createElement(i.Fragment,null,t.props.children):null;for(const l of n)if(l.props.condition)return i.createElement(i.Fragment,null,l.props.children);return o?i.createElement(i.Fragment,null,o.props.children):null};w.displayName="If",w.Then=M,w.ElseIf=$,w.Else=I,w.createTyped=function(){return{If:w,Then:M,ElseIf:$,Else:I}};const re=({condition:e,children:r})=>e?i.createElement(i.Fragment,null,r):null,ne=({condition:e,children:r})=>e===!1?i.createElement(i.Fragment,null,r):null,oe=({all:e,any:r,none:t,children:o,fallback:n})=>S(()=>(e&&(r||t)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(e&&e.length>0&&e.every(Boolean)||r&&r.length>0&&r.some(Boolean)||t&&t.length>0&&t.every(l=>!l))),[e,r,t])?i.createElement(i.Fragment,null,o):i.createElement(i.Fragment,null,n||null),le=({data:e,transform:r,render:t,fallback:o})=>{const n=S(()=>r.reduce((l,s)=>s(l),e),[e,r]);return n==null?i.createElement(i.Fragment,null,o||null):i.createElement(i.Fragment,null,t(n))},se=e=>{const{children:r,h:t,w:o,size:n,height:l,width:s,className:c}=e;return i.createElement("div",{style:{width:n||o||s,height:n||t||l,flexShrink:0},className:c},r)},ae=({let:e,props:r,children:t,fallback:o})=>{const n=S(()=>typeof e=="function"?e(r):e,[e,r]);return!t||!Object.keys(n).length?i.createElement(i.Fragment,null,o||null):i.createElement(i.Fragment,null,t(n))};function O(...e){const r=new Set;for(const t of e)if(t){if(typeof t=="string")r.add(t);else if(Array.isArray(t))t.forEach(o=>r.add(o));else if(typeof t=="object")for(const[o,n]of Object.entries(t))n&&r.add(o)}return Array.from(r).join(" ")}const ie=e=>typeof e=="object"&&!!e,P=({className:e,children:r,asWrapper:t=!1})=>{if(!r)return null;if(!e)return i.createElement(v,null,r);const o=typeof e=="string"?e:O(...Object.values(e));if(t)return i.createElement(t===!0?"div":t,{className:o},r);if(q.count(r)>1)return console.error("<Styles>: children has more than one child. Please check your code."),i.createElement(v,null,r);if(U(r)){const n=r;let l=n?.props?.className;return n?.type?.displayName===P.displayName&&ie(l)&&(l=O(...Object.values(l))),G(r,{className:O(o,l)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),i.createElement(v,null,r)};P.displayName="W/Styles";const ue=e=>{const{index:r=0,options:t,next:o,render:n}=e;b(()=>{if(t.length<r+1)throw new Error(`Index ${r} is out of bounds for options array of length ${t.length}. Defaulting to first option.`)},[r,t]);const[l,s]=A(r),c=()=>{s(a=>t.length?o?o(a,t):(a+1)%t.length:a)};return n(t[l],c)},ce=({onIntersect:e,threshold:r=.1,root:t=null,rootMargin:o="0px",triggerOnce:n=!1,disabled:l=!1,children:s,className:c,style:a})=>{const u=N(null),f=N(null),d=N(!1);return b(()=>{if(l||!u.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const m=u.current,h=y=>{y.forEach(E=>{n&&d.current||(e(E,f.current),n&&(d.current=!0,f.current?.unobserve(m)))})};return f.current=new IntersectionObserver(h,{root:t,rootMargin:o,threshold:r}),f.current.observe(m),()=>{f.current&&f.current.disconnect()}},[e,r,t,o,n,l]),b(()=>{n||(d.current=!1)},[n]),i.createElement("div",{ref:u,className:c,style:a},s)};function fe({times:e,children:r}){if(e<=0)return null;const t=[];for(let o=0;o<e;o++)t.push(r(o));return i.createElement(v,null,t)}function de({to:e,children:r,disabled:t=!1}){const[o,n]=A(!1),l=N(e);if(l.current=e,b(()=>{n(!0)},[]),t)return i.createElement(i.Fragment,null,r);if(!o)return null;const s=l.current??document.body;return ee(r,s)}class me extends K{state={error:null};static getDerivedStateFromError(r){return{error:r}}componentDidCatch(r,t){this.props.onError?.(r,t)}reset=()=>{this.setState({error:null})};render(){return this.state.error?this.props.fallback(this.state.error,this.reset):this.props.children}}function he(e){return i.createElement(me,{...e})}function pe(e){const{items:r,renderItem:t,filter:o,renderEmpty:n,sort:l}=e;if(!r)return console.error("ArrayRender: items is null"),null;if(r.length===0)return n?n():null;if(l){let a=[...r];return o&&(a=a.filter(o)),a=a.sort(l),a.length===0?n?n():null:i.createElement(v,null,a.map((u,f)=>t(u,f)))}let s=0;const c=r.map((a,u)=>o&&!o(a)?(s++,null):t(a,u));return i.createElement(v,null,s===r.length?n?n():null:c)}function ge({source:e,format:r,children:t}){const o=S(()=>{if(e instanceof Date)return e;if(typeof e=="string"||typeof e=="number"){const l=new Date(e);return isNaN(l.getTime())?null:l}return null},[e]),n=S(()=>o?r?r(o):o.toLocaleString():null,[o,r]);return!n||!t?null:i.createElement(i.Fragment,null,t(n))}const ye="onChange",Ee="value";function Se(e){const{defaultValue:r,onBeforeChange:t,trigger:o=ye,valuePropName:n=Ee,props:l}=e,s=Object.prototype.hasOwnProperty.call(l,n),[c,a]=A(r),u=s?l[n]:c,f=S(()=>l[o],[l,o]),d=Q(m=>{const h=typeof m=="function"?m(u):m;t&&t(h,u)===!1||(s||a(h),f&&f(h))},[s,t,u,f]);return[u,d]}function we(e,...r){try{const t=e(...r);return t instanceof Promise?t:Promise.resolve(t)}catch(t){return Promise.reject(t)}}const J=typeof Promise.try=="function"?Promise.try.bind(Promise):we,ve=typeof Promise.withResolvers=="function"?Promise.withResolvers.bind(Promise):()=>{let e,r;return{promise:new Promise((t,o)=>{e=t,r=o}),resolve:e,reject:r}};function W(e,r={}){let t=typeof e=="function"?e():e;const o=[],{sideEffect:n,transform:l}=r,s=()=>{const u=t;return l?.get?l.get(u):u},c=u=>{const f=t,d=l?.get?l.get(f):f;t=l?.set?l.set(typeof u=="function"?u(d):u):typeof u=="function"?u(d):u,o.forEach(m=>m()),n&&J(n,t,f).catch(m=>{console.error("Error in external state side effect, Please do it within side effects:",m)})},a=()=>{const u=X(f=>(o.push(f),()=>{const d=o.indexOf(f);d>-1&&o.splice(d,1)}),()=>t,()=>t);return[l?.get?l.get(u):u,c]};return{get:s,set:c,use:a,useGetter:()=>{const[u]=a();return u},__listeners:o}}function Fe(e,r,t){const{storageType:o="local",sideEffect:n,transform:l}=t??{};let s=r;if(typeof window<"u"){const c=(o==="local"?localStorage:sessionStorage).getItem(e);if(c)try{s=JSON.parse(c)}catch(a){console.warn(`Failed to parse ${o}Storage value for key "${e}", using initial state:`,a),s=r}}return W(s,{sideEffect:c=>{typeof window<"u"&&(o==="local"?localStorage:sessionStorage).setItem(e,JSON.stringify(c)),n?.(c)},transform:l})}function be(e,r){const t=r||new Date,o=t.getFullYear(),n=t.getMonth()+1,l=t.getDate(),s=t.getHours(),c=t.getMinutes(),a=t.getSeconds(),u=t.getMilliseconds(),f=t.getDay(),d=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],m=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],y=["January","February","March","April","May","June","July","August","September","October","November","December"],E=m[f],R=d[f],Y=n-1,H=y[Y],z=h[Y],V={YY:o.toString().slice(2),YYYY:o.toString(),M:n.toString(),MM:n.toString().padStart(2,"0"),MMM:z,MMMM:H,D:l.toString(),DD:l.toString().padStart(2,"0"),d:f.toString(),dd:R,ddd:R,dddd:E,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:c.toString(),mm:c.toString().padStart(2,"0"),s:a.toString(),ss:a.toString().padStart(2,"0"),SSS:u.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return e.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,Z=>V[Z])}class Ne{count=0;next(){return this.count++}}const D=["base","xs","sm","md","lg","xl","2xl","3xl"],L={xs:475,sm:640,md:768,lg:1024,xl:1280,"2xl":1536,"3xl":1920};function p(e,r,t){t&&(e[r]||(e[r]=[]),e[r].push(t))}function De(e){return e==null?!1:typeof e=="string"?e.trim().length>0:Array.isArray(e)?e.length>0:!0}function Ae(e){return e!=null}function g(e,r){return`${String(e)} ${r}`}const k={email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,url:/^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,phone:/^1[3-9]\d{9}$/};function T(e,r,t,o,n){const l=De(r),s=Ae(r);if(t.required&&!l){p(n,e,t.message??g(e,"\u4E3A\u5FC5\u586B\u9879"));return}if(!(!s&&!t.required)){if(t.dependsOn){const c=t.dependsOn(o);c===!1?p(n,e,t.message??g(e,"\u4F9D\u8D56\u6761\u4EF6\u672A\u6EE1\u8DB3")):typeof c=="string"&&p(n,e,c)}if(typeof r=="string"){const c=t,{len:a,min:u,max:f,regex:d,email:m,url:h,phone:y}=c;typeof a=="number"&&r.length!==a&&p(n,e,t.message??g(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${a}`)),typeof u=="number"&&r.length<u&&p(n,e,t.message??g(e,`\u957F\u5EA6\u4E0D\u80FD\u5C11\u4E8E ${u}`)),typeof f=="number"&&r.length>f&&p(n,e,t.message??g(e,`\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${f}`)),d&&!d.test(r)&&p(n,e,t.message??g(e,"\u683C\u5F0F\u4E0D\u6B63\u786E")),m&&!k.email.test(r)&&p(n,e,t.message??g(e,"\u4E0D\u662F\u6709\u6548\u7684\u90AE\u7BB1")),h&&!k.url.test(r)&&p(n,e,t.message??g(e,"\u4E0D\u662F\u6709\u6548\u7684URL")),y&&!k.phone.test(r)&&p(n,e,t.message??g(e,"\u4E0D\u662F\u6709\u6548\u7684\u624B\u673A\u53F7"))}if(typeof r=="number"){const c=t,{min:a,max:u}=c;typeof a=="number"&&r<a&&p(n,e,t.message??g(e,`\u4E0D\u80FD\u5C0F\u4E8E ${a}`)),typeof u=="number"&&r>u&&p(n,e,t.message??g(e,`\u4E0D\u80FD\u5927\u4E8E ${u}`))}if(Array.isArray(r)){const c=t,{len:a,min:u,max:f,unique:d,elementRule:m}=c;typeof a=="number"&&r.length!==a&&p(n,e,t.message??g(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${a}`)),typeof u=="number"&&r.length<u&&p(n,e,t.message??g(e,`\u957F\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${u}`)),typeof f=="number"&&r.length>f&&p(n,e,t.message??g(e,`\u957F\u5EA6\u4E0D\u80FD\u5927\u4E8E ${f}`)),d&&new Set(r).size!==r.length&&p(n,e,t.message??g(e,"\u5143\u7D20\u5FC5\u987B\u552F\u4E00")),m&&r.forEach((h,y)=>{T(`${String(e)}[${y}]`,h,m,o,n)})}if(t.validator){const c=t.validator?.(r,o);c===!1?p(n,e,t.message??g(e,"\u6821\u9A8C\u672A\u901A\u8FC7")):typeof c=="string"&&p(n,e,c)}}}function xe(e,r){const t={};for(const n in r){const l=n,s=r[l];if(!s)continue;const c=e[l];if(Array.isArray(s))for(const a of s)T(l,c,a,e,t);else T(l,c,s,e,t)}const o=Object.values(t).reduce((n,l)=>(l&&n.push(...l),n),[]);return o.length>0?{valid:!1,errors:o,fieldErrors:t}:{valid:!0,data:e}}const Ce=[...D].reverse(),_=typeof window<"u";function B(e,r){for(const t of Ce){const o=e[t];if(o!==void 0&&!Number.isNaN(o)&&r>=o)return t}return"base"}function Me(e=L){const r=S(()=>JSON.stringify(e),[e]),t=N(e);t.current=e;const[o,n]=A(()=>_?B(e,window.innerWidth):"base");return b(()=>{if(!_)return;let l=[],s=[];const c=()=>{s.forEach(h=>h()),l=[],s=[];const a=t.current,u=B(a,window.innerWidth);n(u);const f=D.indexOf(u),d=D[f+1];if(d&&a[d]!==void 0){const h=a[d];if(Number.isNaN(h))throw new Error(`Invalid breakpoint value for ${d}: ${a[d]}`);{const y=window.matchMedia(`(min-width: ${h}px)`);l.push(y);const E=()=>c();y.addEventListener("change",E),s.push(()=>y.removeEventListener("change",E))}}const m=D[f-1];if(m&&a[m]!==void 0){const h=a[m];if(Number.isNaN(h))throw new Error(`Invalid breakpoint value for ${m}: ${a[m]}`);{const y=window.matchMedia(`(max-width: ${h-1}px)`);l.push(y);const E=()=>c();y.addEventListener("change",E),s.push(()=>y.removeEventListener("change",E))}}};return c(),()=>{s.forEach(a=>a())}},[r]),o}export{pe as ArrayRender,he as Boundary,Ne as Counter,ge as DateRender,L as DefBreakpointDesc,ne as False,w as If,ce as Observer,le as Pipe,de as Portal,fe as Repeat,ae as Scope,se as SizeBox,P as Styles,F as Switch,ue as Toggle,re as True,oe as When,D as breakpoints,j as childrenLoop,W as createExternalState,Fe as createStorageState,O as cx,be as formatDate,B as getCurrentBreakpoint,xe as ruleChecker,J as safePromiseTry,ve as safePromiseWithResolvers,Se as useControlled,Me as useScreen};
1
+ import u,{useMemo as S,Fragment as v,Children as q,isValidElement as G,cloneElement as K,useEffect as b,useState as A,useRef as N,Component as Q,useCallback as U,useSyncExternalStore as X}from"react";import{createPortal as ee}from"react-dom";function j(e,n){if(e===void 0)return;let t=0;if(Array.isArray(e)){for(const o of e)if(n(o,t++)===!1)break}else n(e,t)}const te=(e,n)=>e===n,x=e=>u.createElement(u.Fragment,null,e.children);x.displayName="Switch_Case";const C=e=>u.createElement(u.Fragment,null,e.children);C.displayName="Switch_Default";const F=e=>{const{value:n,compare:t=te,children:o,strict:r=!1}=e,l=new Set;let s=null,i=null,a=!1;return j(o,(f,d)=>{if(!u.isValidElement(f))throw new Error(`Switch Children only accepts valid React elements at index ${d}`);const c=f.type;if(c.displayName===x.displayName){const m=f.props;if(l.has(m.value))throw new Error(`Switch found duplicate Case value at index ${d}: ${JSON.stringify(m.value)}${r?" (detected in strict mode)":""}`);if(l.add(m.value),!s&&t(n,m.value)&&(s=m.children,r===!1))return!1}else if(c.displayName===C.displayName){if(a)throw new Error(`Switch can only have one Default child at index ${d}`);if(a=!0,i=f.props.children,!r&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(c.displayName||c.name||c)} at index ${d}`)}),u.createElement(u.Fragment,null,s??i)};F.displayName="Switch",F.Case=x,F.Default=C,F.createTyped=function(){return{Switch:F,Case:x,Default:C}};const M=e=>u.createElement(u.Fragment,null,e.children),I=({children:e})=>u.createElement(u.Fragment,null,e),O=e=>u.createElement(u.Fragment,null,e.children);M.displayName="If_Then",I.displayName="If_Else",O.displayName="If_ElseIf";const w=({condition:e,children:n})=>{let t=null,o=null;const r=[];if(u.Children.forEach(n,l=>{if(!u.isValidElement(l))throw new Error("If component only accepts valid React elements");const s=l.type;if(s.displayName===M.displayName){if(t)throw new Error("If component can only have one Then child");t=l}else if(s.displayName===O.displayName)r.push(l);else if(s.displayName===I.displayName){if(o)throw new Error("If component can only have one Else child");o=l}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),e)return t?u.createElement(u.Fragment,null,t.props.children):null;for(const l of r)if(l.props.condition)return u.createElement(u.Fragment,null,l.props.children);return o?u.createElement(u.Fragment,null,o.props.children):null};w.displayName="If",w.Then=M,w.ElseIf=O,w.Else=I,w.createTyped=function(){return{If:w,Then:M,ElseIf:O,Else:I}};const ne=({condition:e,children:n})=>e?u.createElement(u.Fragment,null,n):null,re=({condition:e,children:n})=>e===!1?u.createElement(u.Fragment,null,n):null,oe=({all:e,any:n,none:t,children:o,fallback:r})=>S(()=>(e&&(n||t)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(e&&e.length>0&&e.every(Boolean)||n&&n.length>0&&n.some(Boolean)||t&&t.length>0&&t.every(l=>!l))),[e,n,t])?u.createElement(u.Fragment,null,o):u.createElement(u.Fragment,null,r||null),le=({data:e,transform:n,render:t,fallback:o})=>{const r=S(()=>n.reduce((l,s)=>s(l),e),[e,n]);return r==null?u.createElement(u.Fragment,null,o||null):u.createElement(u.Fragment,null,t(r))},se=e=>{const{children:n,h:t,w:o,size:r,height:l,width:s,className:i}=e;return u.createElement("div",{style:{width:r||o||s,height:r||t||l,flexShrink:0},className:i},n)},ae=({let:e,props:n,children:t,fallback:o})=>{const r=S(()=>typeof e=="function"?e(n):e,[e,n]);return!t||!Object.keys(r).length?u.createElement(u.Fragment,null,o||null):u.createElement(u.Fragment,null,t(r))};function $(...e){const n=new Set;for(const t of e)if(t){if(typeof t=="string")n.add(t);else if(Array.isArray(t))t.forEach(o=>n.add(o));else if(typeof t=="object")for(const[o,r]of Object.entries(t))r&&n.add(o)}return Array.from(n).join(" ")}const ue=e=>typeof e=="object"&&!!e,P=({className:e,children:n,asWrapper:t=!1})=>{if(!n)return null;if(!e)return u.createElement(v,null,n);const o=typeof e=="string"?e:$(...Object.values(e));if(t)return u.createElement(t===!0?"div":t,{className:o},n);if(q.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),u.createElement(v,null,n);if(G(n)){const r=n;let l=r?.props?.className;return r?.type?.displayName===P.displayName&&ue(l)&&(l=$(...Object.values(l))),K(n,{className:$(o,l)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),u.createElement(v,null,n)};P.displayName="W/Styles";const ie=e=>{const{index:n=0,options:t,next:o,render:r}=e;b(()=>{if(t.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${t.length}. Defaulting to first option.`)},[n,t]);const[l,s]=A(n),i=()=>{s(a=>t.length?o?o(a,t):(a+1)%t.length:a)};return r(t[l],i)},ce=({onIntersect:e,threshold:n=.1,root:t=null,rootMargin:o="0px",triggerOnce:r=!1,disabled:l=!1,children:s,className:i,style:a})=>{const f=N(null),d=N(null),c=N(!1);return b(()=>{if(l||!f.current)return;if(!window.IntersectionObserver){console.warn("IntersectionObserver is not supported in this browser");return}const m=f.current,h=p=>{p.forEach(E=>{r&&c.current||(e(E,d.current),r&&(c.current=!0,d.current?.unobserve(m)))})};return d.current=new IntersectionObserver(h,{root:t,rootMargin:o,threshold:n}),d.current.observe(m),()=>{d.current&&d.current.disconnect()}},[e,n,t,o,r,l]),b(()=>{r||(c.current=!1)},[r]),u.createElement("div",{ref:f,className:i,style:a},s)};function fe({times:e,children:n}){if(e<=0)return null;const t=[];for(let o=0;o<e;o++)t.push(n(o));return u.createElement(v,null,t)}function de({to:e,children:n,disabled:t=!1}){const[o,r]=A(!1),l=N(e);if(l.current=e,b(()=>{r(!0)},[]),t)return u.createElement(u.Fragment,null,n);if(!o)return null;const s=l.current??document.body;return ee(n,s)}class me extends Q{state={error:null};static getDerivedStateFromError(n){return{error:n}}componentDidCatch(n,t){this.props.onError?.(n,t)}reset=()=>{this.setState({error:null})};render(){return this.state.error?this.props.fallback(this.state.error,this.reset):this.props.children}}function he(e){return u.createElement(me,{...e})}function pe(e){const{items:n,renderItem:t,filter:o,renderEmpty:r,sort:l}=e;if(!n)return console.error("ArrayRender: items is null"),null;if(n.length===0)return r?r():null;if(l){let a=[...n];return o&&(a=a.filter(o)),a=a.sort(l),a.length===0?r?r():null:u.createElement(v,null,a.map((f,d)=>t(f,d)))}let s=0;const i=n.map((a,f)=>o&&!o(a)?(s++,null):t(a,f));return u.createElement(v,null,s===n.length?r?r():null:i)}function ge({source:e,format:n,children:t}){const o=S(()=>{if(e instanceof Date)return e;if(typeof e=="string"||typeof e=="number"){const l=new Date(e);return isNaN(l.getTime())?null:l}return null},[e]),r=S(()=>o?n?n(o):o.toLocaleString():null,[o,n]);return!r||!t?null:u.createElement(u.Fragment,null,t(r))}const ye="onChange",Ee="value";function Se(e){const{defaultValue:n,onBeforeChange:t,trigger:o=ye,valuePropName:r=Ee,props:l}=e,s=Object.prototype.hasOwnProperty.call(l,r),[i,a]=A(n),f=s?l[r]:i,d=S(()=>l[o],[l,o]),c=U(m=>{const h=typeof m=="function"?m(f):m;t&&t(h,f)===!1||(s||a(h),d&&d(h))},[s,t,f,d]);return[f,c]}function we(e,...n){try{const t=e(...n);return t instanceof Promise?t:Promise.resolve(t)}catch(t){return Promise.reject(t)}}const J=typeof Promise.try=="function"?Promise.try.bind(Promise):we,ve=typeof Promise.withResolvers=="function"?Promise.withResolvers.bind(Promise):()=>{let e,n;return{promise:new Promise((t,o)=>{e=t,n=o}),resolve:e,reject:n}};function W(e,n={}){let t=typeof e=="function"?e():e;const o=[],{onSet:r,onChange:l,transform:s}=n,i=(c,m,h)=>{c&&J(c,m,h).catch(p=>{console.error("Error in external state callback, Please do it within side effects:",p)})},a=()=>{const c=t;return s?.get?s.get(c):c},f=c=>{const m=t,h=s?.get?s.get(m):m;t=s?.set?s.set(typeof c=="function"?c(h):c):typeof c=="function"?c(h):c,o.forEach(p=>p()),i(r,t,m),Object.is(t,m)||i(l,t,m)},d=()=>{const c=X(m=>(o.push(m),()=>{const h=o.indexOf(m);h>-1&&o.splice(h,1)}),()=>t,()=>t);return[s?.get?s.get(c):c,f]};return{get:a,set:f,use:d,useState:d,useGetter:()=>{const[c]=d();return c},__listeners:o}}function Fe(e,n,t){const{storageType:o="local",onSet:r,onChange:l,transform:s}=t??{};let i=n;if(typeof window<"u"){const a=(o==="local"?localStorage:sessionStorage).getItem(e);if(a)try{i=JSON.parse(a)}catch(f){console.warn(`Failed to parse ${o}Storage value for key "${e}", using initial state:`,f),i=n}}return W(i,{onSet:(a,f)=>{typeof window<"u"&&(o==="local"?localStorage:sessionStorage).setItem(e,JSON.stringify(a)),r?.(a,f)},onChange:l,transform:s})}function be(e,n){const t=n||new Date,o=t.getFullYear(),r=t.getMonth()+1,l=t.getDate(),s=t.getHours(),i=t.getMinutes(),a=t.getSeconds(),f=t.getMilliseconds(),d=t.getDay(),c=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],m=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],h=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],p=["January","February","March","April","May","June","July","August","September","October","November","December"],E=m[d],R=c[d],Y=r-1,H=p[Y],z=h[Y],V={YY:o.toString().slice(2),YYYY:o.toString(),M:r.toString(),MM:r.toString().padStart(2,"0"),MMM:z,MMMM:H,D:l.toString(),DD:l.toString().padStart(2,"0"),d:d.toString(),dd:R,ddd:R,dddd:E,H:s.toString(),HH:s.toString().padStart(2,"0"),h:(s%12).toString(),hh:(s%12).toString().padStart(2,"0"),m:i.toString(),mm:i.toString().padStart(2,"0"),s:a.toString(),ss:a.toString().padStart(2,"0"),SSS:f.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return e.replace(/YYYY|YY|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|SSS|Z{1,2}|A|a/g,Z=>V[Z])}class Ne{count=0;next(){return this.count++}}const D=["base","xs","sm","md","lg","xl","2xl","3xl"],L={xs:475,sm:640,md:768,lg:1024,xl:1280,"2xl":1536,"3xl":1920};function g(e,n,t){t&&(e[n]||(e[n]=[]),e[n].push(t))}function De(e){return e==null?!1:typeof e=="string"?e.trim().length>0:Array.isArray(e)?e.length>0:!0}function Ae(e){return e!=null}function y(e,n){return`${String(e)} ${n}`}const k={email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,url:/^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,phone:/^1[3-9]\d{9}$/};function T(e,n,t,o,r){const l=De(n),s=Ae(n);if(t.required&&!l){g(r,e,t.message??y(e,"\u4E3A\u5FC5\u586B\u9879"));return}if(!(!s&&!t.required)){if(t.dependsOn){const i=t.dependsOn(o);i===!1?g(r,e,t.message??y(e,"\u4F9D\u8D56\u6761\u4EF6\u672A\u6EE1\u8DB3")):typeof i=="string"&&g(r,e,i)}if(typeof n=="string"){const i=t,{len:a,min:f,max:d,regex:c,email:m,url:h,phone:p}=i;typeof a=="number"&&n.length!==a&&g(r,e,t.message??y(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${a}`)),typeof f=="number"&&n.length<f&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5C11\u4E8E ${f}`)),typeof d=="number"&&n.length>d&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${d}`)),c&&!c.test(n)&&g(r,e,t.message??y(e,"\u683C\u5F0F\u4E0D\u6B63\u786E")),m&&!k.email.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684\u90AE\u7BB1")),h&&!k.url.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684URL")),p&&!k.phone.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684\u624B\u673A\u53F7"))}if(typeof n=="number"){const i=t,{min:a,max:f}=i;typeof a=="number"&&n<a&&g(r,e,t.message??y(e,`\u4E0D\u80FD\u5C0F\u4E8E ${a}`)),typeof f=="number"&&n>f&&g(r,e,t.message??y(e,`\u4E0D\u80FD\u5927\u4E8E ${f}`))}if(Array.isArray(n)){const i=t,{len:a,min:f,max:d,unique:c,elementRule:m}=i;typeof a=="number"&&n.length!==a&&g(r,e,t.message??y(e,`\u957F\u5EA6\u5FC5\u987B\u4E3A ${a}`)),typeof f=="number"&&n.length<f&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5C0F\u4E8E ${f}`)),typeof d=="number"&&n.length>d&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5927\u4E8E ${d}`)),c&&new Set(n).size!==n.length&&g(r,e,t.message??y(e,"\u5143\u7D20\u5FC5\u987B\u552F\u4E00")),m&&n.forEach((h,p)=>{T(`${String(e)}[${p}]`,h,m,o,r)})}if(t.validator){const i=t.validator?.(n,o);i===!1?g(r,e,t.message??y(e,"\u6821\u9A8C\u672A\u901A\u8FC7")):typeof i=="string"&&g(r,e,i)}}}function xe(e,n){const t={};for(const r in n){const l=r,s=n[l];if(!s)continue;const i=e[l];if(Array.isArray(s))for(const a of s)T(l,i,a,e,t);else T(l,i,s,e,t)}const o=Object.values(t).reduce((r,l)=>(l&&r.push(...l),r),[]);return o.length>0?{valid:!1,errors:o,fieldErrors:t}:{valid:!0,data:e}}const Ce=[...D].reverse(),_=typeof window<"u";function B(e,n){for(const t of Ce){const o=e[t];if(o!==void 0&&!Number.isNaN(o)&&n>=o)return t}return"base"}function Me(e=L){const n=S(()=>JSON.stringify(e),[e]),t=N(e);t.current=e;const[o,r]=A(()=>_?B(e,window.innerWidth):"base");return b(()=>{if(!_)return;let l=[],s=[];const i=()=>{s.forEach(h=>h()),l=[],s=[];const a=t.current,f=B(a,window.innerWidth);r(f);const d=D.indexOf(f),c=D[d+1];if(c&&a[c]!==void 0){const h=a[c];if(Number.isNaN(h))throw new Error(`Invalid breakpoint value for ${c}: ${a[c]}`);{const p=window.matchMedia(`(min-width: ${h}px)`);l.push(p);const E=()=>i();p.addEventListener("change",E),s.push(()=>p.removeEventListener("change",E))}}const m=D[d-1];if(m&&a[m]!==void 0){const h=a[m];if(Number.isNaN(h))throw new Error(`Invalid breakpoint value for ${m}: ${a[m]}`);{const p=window.matchMedia(`(max-width: ${h-1}px)`);l.push(p);const E=()=>i();p.addEventListener("change",E),s.push(()=>p.removeEventListener("change",E))}}};return i(),()=>{s.forEach(a=>a())}},[n]),o}export{pe as ArrayRender,he as Boundary,Ne as Counter,ge as DateRender,L as DefBreakpointDesc,re as False,w as If,ce as Observer,le as Pipe,de as Portal,fe as Repeat,ae as Scope,se as SizeBox,P as Styles,F as Switch,ie as Toggle,ne as True,oe as When,D as breakpoints,j as childrenLoop,W as createExternalState,Fe as createStorageState,$ as cx,be as formatDate,B as getCurrentBreakpoint,xe as ruleChecker,J as safePromiseTry,ve as safePromiseWithResolvers,Se as useControlled,Me as useScreen};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwog/react",
3
- "version": "1.3.13",
3
+ "version": "1.4.0",
4
4
  "description": "A practical React component library providing declarative flow control and common UI utility components to make your React code more concise and readable.",
5
5
  "keywords": [
6
6
  "react",
@@ -46,6 +46,29 @@ describe("createExternalState", () => {
46
46
  expect(state.get()).toBe("updated");
47
47
  });
48
48
 
49
+ it("测试useState钩子在组件中使用", async () => {
50
+ const initialState = "initial";
51
+ const state = createExternalState(initialState);
52
+
53
+ function TestComponent() {
54
+ const [value, setValue] = state.useState();
55
+ return (
56
+ <div>
57
+ <span data-testid="value">{value}</span>
58
+ <button onClick={() => setValue("updated")}>Update</button>
59
+ </div>
60
+ );
61
+ }
62
+
63
+ const { getByTestId, getByText } = render(<TestComponent />);
64
+ const valueLocator = getByTestId("value");
65
+ const buttonLocator = getByText("Update");
66
+ expect(valueLocator.element().textContent).toBe(initialState);
67
+ await buttonLocator.click();
68
+ expect(valueLocator.element().textContent).toBe("updated");
69
+ expect(state.get()).toBe("updated");
70
+ });
71
+
49
72
  it("测试多个组件共享状态", async () => {
50
73
  const initialState = "initial";
51
74
  const state = createExternalState(initialState);
@@ -120,30 +143,49 @@ describe("createExternalState", () => {
120
143
  expect(state.__listeners.length).toBe(0);
121
144
  });
122
145
 
123
- it("测试副作用函数", () => {
124
- const mockSideEffect = vi.fn((...args) => void 0);
146
+ it("测试 onSet 每次 set 都会触发", () => {
147
+ const mockOnSet = vi.fn((...args) => void 0);
125
148
  const initialState: string = "initial";
126
149
  const state = createExternalState(initialState, {
127
- sideEffect: mockSideEffect,
150
+ onSet: mockOnSet,
128
151
  });
129
152
  state.set("updated");
130
- expect(mockSideEffect).toHaveBeenCalledTimes(1);
131
- expect(mockSideEffect).toHaveBeenCalledWith("updated", initialState);
153
+ expect(mockOnSet).toHaveBeenCalledTimes(1);
154
+ expect(mockOnSet).toHaveBeenCalledWith("updated", initialState);
155
+ state.set("updated");
156
+ expect(mockOnSet).toHaveBeenCalledTimes(2);
157
+ expect(mockOnSet).toHaveBeenCalledWith("updated", "updated");
158
+ state.set("updated2");
159
+ expect(mockOnSet).toHaveBeenCalledTimes(3);
160
+ expect(mockOnSet).toHaveBeenCalledWith("updated2", "updated");
161
+ });
162
+
163
+ it("测试 onChange 仅在值变化时触发", () => {
164
+ const mockOnChange = vi.fn((...args) => void 0);
165
+ const initialState: string = "initial";
166
+ const state = createExternalState(initialState, {
167
+ onChange: mockOnChange,
168
+ });
169
+ state.set("updated");
170
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
171
+ expect(mockOnChange).toHaveBeenCalledWith("updated", initialState);
172
+ state.set("updated");
173
+ expect(mockOnChange).toHaveBeenCalledTimes(1);
132
174
  state.set("updated2");
133
- expect(mockSideEffect).toHaveBeenCalledTimes(2);
134
- expect(mockSideEffect).toHaveBeenCalledWith("updated2", "updated");
175
+ expect(mockOnChange).toHaveBeenCalledTimes(2);
176
+ expect(mockOnChange).toHaveBeenCalledWith("updated2", "updated");
135
177
  });
136
178
 
137
- it("测试异步副作用函数", async () => {
138
- const mockAsyncSideEffect = vi.fn().mockResolvedValue(undefined);
179
+ it("测试异步 onSet 回调", async () => {
180
+ const mockAsyncOnSet = vi.fn().mockResolvedValue(undefined);
139
181
  const initialState: string = "initial";
140
182
  const state = createExternalState(initialState, {
141
- sideEffect: mockAsyncSideEffect,
183
+ onSet: mockAsyncOnSet,
142
184
  });
143
185
 
144
186
  state.set("updated");
145
- expect(mockAsyncSideEffect).toHaveBeenCalledTimes(1);
146
- expect(mockAsyncSideEffect).toHaveBeenCalledWith("updated", initialState);
187
+ expect(mockAsyncOnSet).toHaveBeenCalledTimes(1);
188
+ expect(mockAsyncOnSet).toHaveBeenCalledWith("updated", initialState);
147
189
  });
148
190
 
149
191
  it("测试复杂数据类型", async () => {
@@ -321,17 +363,17 @@ describe("createStorageState", () => {
321
363
  consoleSpy.mockRestore();
322
364
  });
323
365
 
324
- it("测试存储副作用函数", () => {
325
- const mockSideEffect = vi.fn();
366
+ it("测试存储 onSet 回调", () => {
367
+ const mockOnSet = vi.fn();
326
368
  const state = createStorageState("test-key", "initial" as string, {
327
369
  storageType: "local",
328
- sideEffect: mockSideEffect,
370
+ onSet: mockOnSet,
329
371
  });
330
372
 
331
373
  state.set("updated");
332
374
 
333
- expect(mockSideEffect).toHaveBeenCalledTimes(1);
334
- expect(mockSideEffect).toHaveBeenCalledWith("updated");
375
+ expect(mockOnSet).toHaveBeenCalledTimes(1);
376
+ expect(mockOnSet).toHaveBeenCalledWith("updated", "initial");
335
377
  expect(localStorage.getItem("test-key")).toBe('"updated"');
336
378
  });
337
379
 
@@ -1,14 +1,14 @@
1
- import {useSyncExternalStore} from 'react'
2
- import {safePromiseTry} from './promise'
1
+ import { useSyncExternalStore } from 'react'
2
+ import { safePromiseTry } from './promise'
3
3
 
4
4
  /**
5
- * @zh 如果需要在变更状态时执行副作用,可以传入函数,对于异步函数,会在更改状态后执行,不会阻塞状态更新, 尽可能在外部使用useEffect处理异步副作用
6
- * @en If you need to perform side effects when changing the state, you can pass a function. For asynchronous functions, it will be executed after the state changes without blocking the state update, so it's best to use useEffect for handling asynchronous side effects.
5
+ * @zh 状态回调函数。对于异步函数,会在状态更新后执行,不会阻塞状态更新,尽可能在外部使用 useEffect 处理异步副作用。
6
+ * @en State callback function. Async callbacks run after the state update without blocking it; prefer useEffect for async side effects.
7
7
  * @template T The type of the state / 状态的类型
8
8
  * @param newState The new state value / 新的状态值
9
9
  * @param prevState The previous state value / 之前的状态值
10
10
  */
11
- export type ExternalSideEffect<T> = (newState: T, prevState: T) => any | Promise<any>
11
+ export type ExternalStateCallback<T> = (newState: T, prevState: T) => any | Promise<any>
12
12
 
13
13
  /**
14
14
  * @en Transform functions for getting and setting state
@@ -37,10 +37,15 @@ export interface Transform<T, U = T> {
37
37
  */
38
38
  export interface ExternalStateOptions<T, U = T> {
39
39
  /**
40
- * @en Side effect function to run after state changes
41
- * @zh 状态变更后运行的副作用函数
40
+ * @en Callback invoked on every `set` call, even when the value is unchanged
41
+ * @zh 每次调用 `set` 后触发,即使值未发生变化
42
42
  */
43
- sideEffect?: ExternalSideEffect<T>
43
+ onSet?: ExternalStateCallback<T>
44
+ /**
45
+ * @en Callback invoked only when the stored value actually changes
46
+ * @zh 仅在内部存储值发生变化时触发
47
+ */
48
+ onChange?: ExternalStateCallback<T>
44
49
  /**
45
50
  * @en Transform functions for getting and setting state
46
51
  * @zh 用于获取和设置状态的转换函数
@@ -69,12 +74,13 @@ export interface ExternalState<T, U = T> {
69
74
  */
70
75
  set: (newState: U | ((prevState: U) => U)) => void
71
76
 
77
+
72
78
  /**
73
- * @en React Hook for using external state in components
74
- * @zh 在组件中使用外部状态的 React Hook
75
- * @returns Array containing current钣金龙8国际唯一官网 current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
79
+ * @en React Hook for using external state in components.
80
+ * @zh 在组件中使用外部状态的 React Hook
81
+ * @returns Array containing current state and update function, similar to React useState / 包含当前状态和更新函数的数组,类似于 React useState
76
82
  */
77
- use: () => [U, (newState: U | ((prevState: U) => U)) => void]
83
+ useState: () => [U, (newState: U | ((prevState: U) => U)) => void]
78
84
 
79
85
  /**
80
86
  * @zh use的变体,只获取value.
@@ -93,7 +99,7 @@ export interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
93
99
  * ```tsx
94
100
  * // Create an app-level theme state with options
95
101
  * const themeState = createExternalState('light', {
96
- * sideEffect: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
102
+ * onChange: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
97
103
  * transform: {
98
104
  * get: (state) => state.toUpperCase(),
99
105
  * set: (value) => value.toLowerCase()
@@ -106,7 +112,7 @@ export interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
106
112
  *
107
113
  * // Use state in components
108
114
  * function ThemeConsumer() {
109
- * const [theme, setTheme] = themeState.use();
115
+ * const [theme, setTheme] = themeState.useState();
110
116
  *
111
117
  * return (
112
118
  * <div className={theme}>
@@ -125,7 +131,18 @@ export function createExternalState<T, U = T>(
125
131
  let state: T = typeof initialState === 'function' ? (initialState as () => T)() : initialState
126
132
 
127
133
  const storeListeners: (() => void)[] = []
128
- const {sideEffect, transform} = options
134
+ const { onSet, onChange, transform } = options
135
+
136
+ const runCallback = (
137
+ callback: ExternalStateCallback<T> | undefined,
138
+ newState: T,
139
+ prevState: T,
140
+ ) => {
141
+ if (!callback) return
142
+ safePromiseTry(callback, newState, prevState).catch((error) => {
143
+ console.error('Error in external state callback, Please do it within side effects:', error)
144
+ })
145
+ }
129
146
 
130
147
  const get = () => {
131
148
  const currentState = state
@@ -139,26 +156,23 @@ export function createExternalState<T, U = T>(
139
156
  : (prevState as unknown as U)
140
157
  state = transform?.set
141
158
  ? transform.set(
142
- typeof newState === 'function'
143
- ? (newState as (prev: U) => U)(transformedPrevState)
144
- : newState,
145
- )
146
- : ((typeof newState === 'function'
159
+ typeof newState === 'function'
147
160
  ? (newState as (prev: U) => U)(transformedPrevState)
148
- : newState) as unknown as T)
161
+ : newState,
162
+ )
163
+ : ((typeof newState === 'function'
164
+ ? (newState as (prev: U) => U)(transformedPrevState)
165
+ : newState) as unknown as T)
149
166
 
150
167
  storeListeners.forEach((listener) => listener())
151
- if (sideEffect) {
152
- safePromiseTry(sideEffect, state, prevState).catch((error) => {
153
- console.error(
154
- 'Error in external state side effect, Please do it within side effects:',
155
- error,
156
- )
157
- })
168
+
169
+ runCallback(onSet, state, prevState)
170
+ if (!Object.is(state, prevState)) {
171
+ runCallback(onChange, state, prevState)
158
172
  }
159
173
  }
160
174
 
161
- const use = () => {
175
+ const useState = () => {
162
176
  const localState = useSyncExternalStore(
163
177
  (onStoreChange) => {
164
178
  storeListeners.push(onStoreChange)
@@ -179,17 +193,19 @@ export function createExternalState<T, U = T>(
179
193
  ]
180
194
  }
181
195
 
196
+
182
197
  const useGetter = () => {
183
- const [value] = use()
198
+ const [value] = useState()
184
199
  return value
185
200
  }
186
201
 
187
202
  //@ts-expect-error ignore
188
- return {get, set, use, useGetter, __listeners: storeListeners}
203
+ return { get, set, useState, useGetter, __listeners: storeListeners }
189
204
  }
190
205
 
191
206
  export interface StorageStateOptions<T, U> {
192
- sideEffect?: (newState: T) => void
207
+ onSet?: ExternalStateCallback<T>
208
+ onChange?: ExternalStateCallback<T>
193
209
  transform?: Transform<T, U>
194
210
  storageType: 'local' | 'session'
195
211
  }
@@ -199,7 +215,7 @@ export function createStorageState<T, U = T>(
199
215
  initialState: T,
200
216
  options?: StorageStateOptions<T, U>,
201
217
  ) {
202
- const {storageType = 'local', sideEffect, transform} = options ?? {}
218
+ const { storageType = 'local', onSet, onChange, transform } = options ?? {}
203
219
  let _initState: T = initialState
204
220
 
205
221
  // 只在客户端环境中读取存储
@@ -220,14 +236,15 @@ export function createStorageState<T, U = T>(
220
236
  }
221
237
 
222
238
  return createExternalState(_initState, {
223
- sideEffect: (newState) => {
239
+ onSet: (newState, prevState) => {
224
240
  // 只在客户端环境中写入存储
225
241
  if (typeof window !== 'undefined') {
226
242
  const storage = storageType === 'local' ? localStorage : sessionStorage
227
243
  storage.setItem(key, JSON.stringify(newState))
228
244
  }
229
- sideEffect?.(newState)
245
+ onSet?.(newState, prevState)
230
246
  },
247
+ onChange,
231
248
  transform,
232
249
  })
233
250
  }