@wwog/react 1.3.13 → 1.3.14
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 +13 -3
- package/dist/index.d.mts +16 -10
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/utils/createExternalState.test.tsx +36 -17
- package/src/utils/createExternalState.ts +31 -20
package/README.md
CHANGED
|
@@ -649,6 +649,7 @@ const result = ruleChecker(registrationData, rules);
|
|
|
649
649
|
|
|
650
650
|
> v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
|
|
651
651
|
> v1.2.13: add useGetter
|
|
652
|
+
> Breaking: `sideEffect` replaced by `onSet` and `onChange` for clearer callback semantics
|
|
652
653
|
|
|
653
654
|
> 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
655
|
|
|
@@ -656,12 +657,18 @@ const result = ruleChecker(registrationData, rules);
|
|
|
656
657
|
|
|
657
658
|
> Extends from createExternalState and uses storage to persist state, supports `localStorage` and `sessionStorage`
|
|
658
659
|
|
|
660
|
+
- `createStorageState<T>(key, initialState, options?)`: Creates persisted state
|
|
661
|
+
- `options.onSet`: Invoked on every `set()` (storage write happens first, then the user callback)
|
|
662
|
+
- `options.onChange`: Invoked only when the value actually changes
|
|
663
|
+
- `options.storageType`: `'local'` | `'session'`, defaults to `'local'`
|
|
664
|
+
- `options.transform`: Same as `createExternalState`
|
|
665
|
+
|
|
659
666
|
```tsx
|
|
660
667
|
import { createExternalState } from "@wwog/react";
|
|
661
668
|
|
|
662
669
|
// Create a global theme state
|
|
663
670
|
const themeState = createExternalState("light", {
|
|
664
|
-
|
|
671
|
+
onChange: (newTheme, oldTheme) => {
|
|
665
672
|
console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
|
|
666
673
|
},
|
|
667
674
|
});
|
|
@@ -695,14 +702,17 @@ function ReadOnlyThemeConsumer() {
|
|
|
695
702
|
- `createExternalState<T>(initialState, options?)`: Creates a state accessible outside components
|
|
696
703
|
|
|
697
704
|
- `initialState`: Initial state value
|
|
698
|
-
- `options.
|
|
705
|
+
- `options.onSet`: Optional callback invoked on every `set()` call, even when the value is unchanged
|
|
706
|
+
- `options.onChange`: Optional callback invoked only when the stored value actually changes (compared via `Object.is`)
|
|
707
|
+
- Both callbacks receive `(newState, prevState)` as the raw internal values (type `T`, before `transform.get`)
|
|
699
708
|
- Returns an object with methods:
|
|
700
709
|
- `get()`: Get the current state value
|
|
701
710
|
- `set(newState)`: Update the state value
|
|
702
711
|
- `use()`: React Hook, returns `[state, setState]` for using this state in components
|
|
703
712
|
- `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
|
|
704
713
|
- `options.transform`: - `get` - `set`
|
|
705
|
-
|
|
714
|
+
|
|
715
|
+
Use cases:
|
|
706
716
|
|
|
707
717
|
- Global state management (themes, user settings, etc.)
|
|
708
718
|
- 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
|
|
642
|
-
* @en
|
|
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
|
|
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
|
|
675
|
-
* @zh
|
|
674
|
+
* @en Callback invoked on every `set` call, even when the value is unchanged
|
|
675
|
+
* @zh 每次调用 `set` 后触发,即使值未发生变化
|
|
676
676
|
*/
|
|
677
|
-
|
|
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,7 +708,7 @@ 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
|
|
711
|
+
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
707
712
|
*/
|
|
708
713
|
use: () => [U, (newState: U | ((prevState: U) => U)) => void];
|
|
709
714
|
/**
|
|
@@ -721,7 +726,7 @@ interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
|
721
726
|
* ```tsx
|
|
722
727
|
* // Create an app-level theme state with options
|
|
723
728
|
* const themeState = createExternalState('light', {
|
|
724
|
-
*
|
|
729
|
+
* onChange: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
|
|
725
730
|
* transform: {
|
|
726
731
|
* get: (state) => state.toUpperCase(),
|
|
727
732
|
* set: (value) => value.toLowerCase()
|
|
@@ -748,7 +753,8 @@ interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
|
748
753
|
*/
|
|
749
754
|
declare function createExternalState<T, U = T>(initialState: T | (() => T), options?: ExternalStateOptions<T, U>): ExternalState<T, U>;
|
|
750
755
|
interface StorageStateOptions<T, U> {
|
|
751
|
-
|
|
756
|
+
onSet?: ExternalStateCallback<T>;
|
|
757
|
+
onChange?: ExternalStateCallback<T>;
|
|
752
758
|
transform?: Transform<T, U>;
|
|
753
759
|
storageType: 'local' | 'session';
|
|
754
760
|
}
|
|
@@ -866,4 +872,4 @@ declare function getCurrentBreakpoint(breakpointDesc: BreakpointDesc, width: num
|
|
|
866
872
|
declare function useScreen(breakpointDesc?: BreakpointDesc): "base" | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl";
|
|
867
873
|
|
|
868
874
|
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,
|
|
875
|
+
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 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,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,m)=>{if(!u.isValidElement(f))throw new Error(`Switch Children only accepts valid React elements at index ${m}`);const c=f.type;if(c.displayName===x.displayName){const d=f.props;if(l.has(d.value))throw new Error(`Switch found duplicate Case value at index ${m}: ${JSON.stringify(d.value)}${r?" (detected in strict mode)":""}`);if(l.add(d.value),!s&&t(n,d.value)&&(s=d.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 ${m}`);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 ${m}`)}),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(U(n)){const r=n;let l=r?.props?.className;return r?.type?.displayName===P.displayName&&ue(l)&&(l=$(...Object.values(l))),G(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),m=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 d=f.current,h=p=>{p.forEach(E=>{r&&c.current||(e(E,m.current),r&&(c.current=!0,m.current?.unobserve(d)))})};return m.current=new IntersectionObserver(h,{root:t,rootMargin:o,threshold:n}),m.current.observe(d),()=>{m.current&&m.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 K{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,m)=>t(f,m)))}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,m=S(()=>l[o],[l,o]),c=Q(d=>{const h=typeof d=="function"?d(f):d;t&&t(h,f)===!1||(s||a(h),m&&m(h))},[s,t,f,m]);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,d,h)=>{c&&J(c,d,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 d=t,h=s?.get?s.get(d):d;t=s?.set?s.set(typeof c=="function"?c(h):c):typeof c=="function"?c(h):c,o.forEach(p=>p()),i(r,t,d),Object.is(t,d)||i(l,t,d)},m=()=>{const c=X(d=>(o.push(d),()=>{const h=o.indexOf(d);h>-1&&o.splice(h,1)}),()=>t,()=>t);return[s?.get?s.get(c):c,f]};return{get:a,set:f,use:m,useGetter:()=>{const[c]=m();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(),m=t.getDay(),c=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],d=["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=d[m],R=c[m],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:m.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 T={email:/^[^\s@]+@[^\s@]+\.[^\s@]+$/,url:/^(https?:\/\/)?([\w.-]+)\.([a-z]{2,6})([/\w .-]*)*\/?$/,phone:/^1[3-9]\d{9}$/};function k(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:m,regex:c,email:d,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 m=="number"&&n.length>m&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 ${m}`)),c&&!c.test(n)&&g(r,e,t.message??y(e,"\u683C\u5F0F\u4E0D\u6B63\u786E")),d&&!T.email.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684\u90AE\u7BB1")),h&&!T.url.test(n)&&g(r,e,t.message??y(e,"\u4E0D\u662F\u6709\u6548\u7684URL")),p&&!T.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:m,unique:c,elementRule:d}=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 m=="number"&&n.length>m&&g(r,e,t.message??y(e,`\u957F\u5EA6\u4E0D\u80FD\u5927\u4E8E ${m}`)),c&&new Set(n).size!==n.length&&g(r,e,t.message??y(e,"\u5143\u7D20\u5FC5\u987B\u552F\u4E00")),d&&n.forEach((h,p)=>{k(`${String(e)}[${p}]`,h,d,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)k(l,i,a,e,t);else k(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 m=D.indexOf(f),c=D[m+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 d=D[m-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 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.
|
|
3
|
+
"version": "1.3.14",
|
|
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",
|
|
@@ -120,30 +120,49 @@ describe("createExternalState", () => {
|
|
|
120
120
|
expect(state.__listeners.length).toBe(0);
|
|
121
121
|
});
|
|
122
122
|
|
|
123
|
-
it("
|
|
124
|
-
const
|
|
123
|
+
it("测试 onSet 每次 set 都会触发", () => {
|
|
124
|
+
const mockOnSet = vi.fn((...args) => void 0);
|
|
125
125
|
const initialState: string = "initial";
|
|
126
126
|
const state = createExternalState(initialState, {
|
|
127
|
-
|
|
127
|
+
onSet: mockOnSet,
|
|
128
128
|
});
|
|
129
129
|
state.set("updated");
|
|
130
|
-
expect(
|
|
131
|
-
expect(
|
|
130
|
+
expect(mockOnSet).toHaveBeenCalledTimes(1);
|
|
131
|
+
expect(mockOnSet).toHaveBeenCalledWith("updated", initialState);
|
|
132
|
+
state.set("updated");
|
|
133
|
+
expect(mockOnSet).toHaveBeenCalledTimes(2);
|
|
134
|
+
expect(mockOnSet).toHaveBeenCalledWith("updated", "updated");
|
|
135
|
+
state.set("updated2");
|
|
136
|
+
expect(mockOnSet).toHaveBeenCalledTimes(3);
|
|
137
|
+
expect(mockOnSet).toHaveBeenCalledWith("updated2", "updated");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("测试 onChange 仅在值变化时触发", () => {
|
|
141
|
+
const mockOnChange = vi.fn((...args) => void 0);
|
|
142
|
+
const initialState: string = "initial";
|
|
143
|
+
const state = createExternalState(initialState, {
|
|
144
|
+
onChange: mockOnChange,
|
|
145
|
+
});
|
|
146
|
+
state.set("updated");
|
|
147
|
+
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(mockOnChange).toHaveBeenCalledWith("updated", initialState);
|
|
149
|
+
state.set("updated");
|
|
150
|
+
expect(mockOnChange).toHaveBeenCalledTimes(1);
|
|
132
151
|
state.set("updated2");
|
|
133
|
-
expect(
|
|
134
|
-
expect(
|
|
152
|
+
expect(mockOnChange).toHaveBeenCalledTimes(2);
|
|
153
|
+
expect(mockOnChange).toHaveBeenCalledWith("updated2", "updated");
|
|
135
154
|
});
|
|
136
155
|
|
|
137
|
-
it("
|
|
138
|
-
const
|
|
156
|
+
it("测试异步 onSet 回调", async () => {
|
|
157
|
+
const mockAsyncOnSet = vi.fn().mockResolvedValue(undefined);
|
|
139
158
|
const initialState: string = "initial";
|
|
140
159
|
const state = createExternalState(initialState, {
|
|
141
|
-
|
|
160
|
+
onSet: mockAsyncOnSet,
|
|
142
161
|
});
|
|
143
162
|
|
|
144
163
|
state.set("updated");
|
|
145
|
-
expect(
|
|
146
|
-
expect(
|
|
164
|
+
expect(mockAsyncOnSet).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(mockAsyncOnSet).toHaveBeenCalledWith("updated", initialState);
|
|
147
166
|
});
|
|
148
167
|
|
|
149
168
|
it("测试复杂数据类型", async () => {
|
|
@@ -321,17 +340,17 @@ describe("createStorageState", () => {
|
|
|
321
340
|
consoleSpy.mockRestore();
|
|
322
341
|
});
|
|
323
342
|
|
|
324
|
-
it("
|
|
325
|
-
const
|
|
343
|
+
it("测试存储 onSet 回调", () => {
|
|
344
|
+
const mockOnSet = vi.fn();
|
|
326
345
|
const state = createStorageState("test-key", "initial" as string, {
|
|
327
346
|
storageType: "local",
|
|
328
|
-
|
|
347
|
+
onSet: mockOnSet,
|
|
329
348
|
});
|
|
330
349
|
|
|
331
350
|
state.set("updated");
|
|
332
351
|
|
|
333
|
-
expect(
|
|
334
|
-
expect(
|
|
352
|
+
expect(mockOnSet).toHaveBeenCalledTimes(1);
|
|
353
|
+
expect(mockOnSet).toHaveBeenCalledWith("updated", "initial");
|
|
335
354
|
expect(localStorage.getItem("test-key")).toBe('"updated"');
|
|
336
355
|
});
|
|
337
356
|
|
|
@@ -2,13 +2,13 @@ import {useSyncExternalStore} from 'react'
|
|
|
2
2
|
import {safePromiseTry} from './promise'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* @zh
|
|
6
|
-
* @en
|
|
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
|
|
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
|
|
41
|
-
* @zh
|
|
40
|
+
* @en Callback invoked on every `set` call, even when the value is unchanged
|
|
41
|
+
* @zh 每次调用 `set` 后触发,即使值未发生变化
|
|
42
42
|
*/
|
|
43
|
-
|
|
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 用于获取和设置状态的转换函数
|
|
@@ -72,7 +77,7 @@ export interface ExternalState<T, U = T> {
|
|
|
72
77
|
/**
|
|
73
78
|
* @en React Hook for using external state in components
|
|
74
79
|
* @zh 在组件中使用外部状态的 React Hook
|
|
75
|
-
* @returns Array containing current
|
|
80
|
+
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
76
81
|
*/
|
|
77
82
|
use: () => [U, (newState: U | ((prevState: U) => U)) => void]
|
|
78
83
|
|
|
@@ -93,7 +98,7 @@ export interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
|
93
98
|
* ```tsx
|
|
94
99
|
* // Create an app-level theme state with options
|
|
95
100
|
* const themeState = createExternalState('light', {
|
|
96
|
-
*
|
|
101
|
+
* onChange: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
|
|
97
102
|
* transform: {
|
|
98
103
|
* get: (state) => state.toUpperCase(),
|
|
99
104
|
* set: (value) => value.toLowerCase()
|
|
@@ -125,7 +130,14 @@ export function createExternalState<T, U = T>(
|
|
|
125
130
|
let state: T = typeof initialState === 'function' ? (initialState as () => T)() : initialState
|
|
126
131
|
|
|
127
132
|
const storeListeners: (() => void)[] = []
|
|
128
|
-
const {
|
|
133
|
+
const {onSet, onChange, transform} = options
|
|
134
|
+
|
|
135
|
+
const runCallback = (callback: ExternalStateCallback<T> | undefined, newState: T, prevState: T) => {
|
|
136
|
+
if (!callback) return
|
|
137
|
+
safePromiseTry(callback, newState, prevState).catch((error) => {
|
|
138
|
+
console.error('Error in external state callback, Please do it within side effects:', error)
|
|
139
|
+
})
|
|
140
|
+
}
|
|
129
141
|
|
|
130
142
|
const get = () => {
|
|
131
143
|
const currentState = state
|
|
@@ -148,13 +160,10 @@ export function createExternalState<T, U = T>(
|
|
|
148
160
|
: newState) as unknown as T)
|
|
149
161
|
|
|
150
162
|
storeListeners.forEach((listener) => listener())
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
error,
|
|
156
|
-
)
|
|
157
|
-
})
|
|
163
|
+
|
|
164
|
+
runCallback(onSet, state, prevState)
|
|
165
|
+
if (!Object.is(state, prevState)) {
|
|
166
|
+
runCallback(onChange, state, prevState)
|
|
158
167
|
}
|
|
159
168
|
}
|
|
160
169
|
|
|
@@ -189,7 +198,8 @@ export function createExternalState<T, U = T>(
|
|
|
189
198
|
}
|
|
190
199
|
|
|
191
200
|
export interface StorageStateOptions<T, U> {
|
|
192
|
-
|
|
201
|
+
onSet?: ExternalStateCallback<T>
|
|
202
|
+
onChange?: ExternalStateCallback<T>
|
|
193
203
|
transform?: Transform<T, U>
|
|
194
204
|
storageType: 'local' | 'session'
|
|
195
205
|
}
|
|
@@ -199,7 +209,7 @@ export function createStorageState<T, U = T>(
|
|
|
199
209
|
initialState: T,
|
|
200
210
|
options?: StorageStateOptions<T, U>,
|
|
201
211
|
) {
|
|
202
|
-
const {storageType = 'local',
|
|
212
|
+
const {storageType = 'local', onSet, onChange, transform} = options ?? {}
|
|
203
213
|
let _initState: T = initialState
|
|
204
214
|
|
|
205
215
|
// 只在客户端环境中读取存储
|
|
@@ -220,14 +230,15 @@ export function createStorageState<T, U = T>(
|
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
return createExternalState(_initState, {
|
|
223
|
-
|
|
233
|
+
onSet: (newState, prevState) => {
|
|
224
234
|
// 只在客户端环境中写入存储
|
|
225
235
|
if (typeof window !== 'undefined') {
|
|
226
236
|
const storage = storageType === 'local' ? localStorage : sessionStorage
|
|
227
237
|
storage.setItem(key, JSON.stringify(newState))
|
|
228
238
|
}
|
|
229
|
-
|
|
239
|
+
onSet?.(newState, prevState)
|
|
230
240
|
},
|
|
241
|
+
onChange,
|
|
231
242
|
transform,
|
|
232
243
|
})
|
|
233
244
|
}
|