@wwog/react 1.2.10 → 1.2.11

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/dist/index.d.mts CHANGED
@@ -462,7 +462,7 @@ type CreateStateListener<T> = (state: T) => void;
462
462
  * @param newState The new state value / 新的状态值
463
463
  * @param prevState The previous state value / 之前的状态值
464
464
  */
465
- type ExternalSideEffect<T> = (newState: T, prevState: T) => void | Promise<void>;
465
+ type ExternalSideEffect<T> = (newState: T, prevState: T) => any | Promise<any>;
466
466
  /**
467
467
  * @en External state management interface
468
468
  * @zh 外部状态管理接口
@@ -488,6 +488,9 @@ interface ExternalState<T> {
488
488
  */
489
489
  use: () => [T, (newState: T) => void];
490
490
  }
491
+ interface ExternalWithKernel<T> extends ExternalState<T> {
492
+ __listeners: CreateStateListener<T>[];
493
+ }
491
494
  /**
492
495
  *
493
496
  * @example
@@ -563,4 +566,4 @@ declare class Counter {
563
566
  declare const safePromiseTry: <T, U extends unknown[]>(callbackFn: (...args: U) => T | PromiseLike<T>, ...args: U) => Promise<Awaited<T>>;
564
567
 
565
568
  export { ArrayRender, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, childrenLoop, createExternalState, cx, formatDate, safePromiseTry, useControlled };
566
- export type { ArrayRenderProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, FalseProps, IfProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, TrueProps, UseControlledOptions, WhenProps };
569
+ export type { ArrayRenderProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalWithKernel, FalseProps, IfProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, TrueProps, UseControlledOptions, WhenProps };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import r,{useMemo as p,Children as J,Fragment as S,isValidElement as R,cloneElement as W,useEffect as H,useState as x,useCallback as _}from"react";function C(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const l of t)if(n(l,e++)===!1)break}else n(t,e)}const B=(t,n)=>t===n,E=t=>r.createElement(r.Fragment,null,t.children);E.displayName="Switch_Case";const w=t=>r.createElement(r.Fragment,null,t.children);w.displayName="Switch_Default";const y=t=>{const{value:n,compare:e=B,children:l,strict:o=!1}=t,a=new Set;let s=null,c=null,i=!1;return C(l,(d,u)=>{if(!r.isValidElement(d))throw new Error(`Switch Children only accepts valid React elements at index ${u}`);const m=d.type;if(m.displayName===E.displayName){const f=d.props;if(a.has(f.value))throw new Error(`Switch found duplicate Case value at index ${u}: ${JSON.stringify(f.value)}${o?" (detected in strict mode)":""}`);if(a.add(f.value),!s&&e(n,f.value)&&(s=f.children,o===!1))return!1}else if(m.displayName===w.displayName){if(i)throw new Error(`Switch can only have one Default child at index ${u}`);if(i=!0,c=d.props.children,!o&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(m.displayName||m.name||m)} at index ${u}`)}),r.createElement(r.Fragment,null,s??c)};y.displayName="Switch",y.Case=E,y.Default=w,y.createTyped=function(){return{Switch:y,Case:E,Default:w}};const N=t=>r.createElement(r.Fragment,null,t.children),v=({children:t})=>r.createElement(r.Fragment,null,t),F=t=>r.createElement(r.Fragment,null,t.children);N.displayName="If_Then",v.displayName="If_Else",F.displayName="If_ElseIf";const h=({condition:t,children:n})=>{let e=null,l=null;const o=[];if(r.Children.forEach(n,a=>{if(!r.isValidElement(a))throw new Error("If component only accepts valid React elements");const s=a.type;if(s.displayName===N.displayName){if(e)throw new Error("If component can only have one Then child");e=a}else if(s.displayName===F.displayName)o.push(a);else if(s.displayName===v.displayName){if(l)throw new Error("If component can only have one Else child");l=a}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),t)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const a of o)if(a.props.condition)return r.createElement(r.Fragment,null,a.props.children);return l?r.createElement(r.Fragment,null,l.props.children):null};h.displayName="If",h.Then=N,h.ElseIf=F,h.Else=v,h.createTyped=function(){return{If:h,Then:N,ElseIf:F,Else:v}};const V=({condition:t,children:n})=>t?r.createElement(r.Fragment,null,n):null,Z=({condition:t,children:n})=>t===!1?r.createElement(r.Fragment,null,n):null,z=({all:t,any:n,none:e,children:l,fallback:o})=>p(()=>(t&&(n||e)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(t&&t.length>0&&t.every(Boolean)||n&&n.length>0&&n.some(Boolean)||e&&e.length>0&&e.every(a=>!a))),[t,n,e])?r.createElement(r.Fragment,null,l):r.createElement(r.Fragment,null,o||null),L=({data:t,transform:n,render:e,fallback:l})=>{const o=p(()=>n.reduce((a,s)=>s(a),t),[t,n]);return o==null?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))},q=t=>{const{children:n,h:e,w:l,size:o,height:a,width:s,className:c}=t;return r.createElement("div",{style:{width:o||l||s,height:o||e||a,flexShrink:0},className:c},n)},G=({let:t,props:n,children:e,fallback:l})=>{const o=p(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))};function M(...t){const n=new Set;for(const e of t)if(e){if(typeof e=="string")n.add(e);else if(Array.isArray(e))e.forEach(l=>n.add(l));else if(typeof e=="object")for(const[l,o]of Object.entries(e))o&&n.add(l)}return Array.from(n).join(" ")}const K=t=>typeof t=="object"&&!!t,b=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(J.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),r.createElement(S,null,n);if(!t)return r.createElement(S,null,n);const l=typeof t=="string"?t:M(...Object.values(t));if(e)return r.createElement(e===!0?"div":e,{className:l},n);if(R(n)){const o=n;let a=o?.props?.className;return o?.type?.displayName===b.displayName&&K(a)&&(a=M(...Object.values(a))),W(n,{className:M(l,a)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),r.createElement(S,null,n)};b.displayName="W/Styles";const Q=t=>{const{index:n=0,options:e,next:l,render:o}=t;H(()=>{if(e.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${e.length}. Defaulting to first option.`)},[n,e]);const[a,s]=x(n),c=()=>{s(i=>e.length?l?l(i,e):(i+1)%e.length:i)};return o(e[a],c)};function U(t){const{items:n,renderItem:e,filter:l}=t;return n?r.createElement(S,null,n.map((o,a)=>l&&!l(o)?null:e(o,a))):(console.error("ArrayRender: items is null"),null)}function X({source:t,format:n,children:e}){const l=p(()=>{if(t instanceof Date)return t;if(typeof t=="string"||typeof t=="number"){const a=new Date(t);return isNaN(a.getTime())?null:a}return null},[t]),o=p(()=>l?n?n(l):l.toLocaleString():null,[l,n]);return!o||!e?null:r.createElement(r.Fragment,null,e(o))}const $="onChange",ee="value";function te(t){const{defaultValue:n,onBeforeChange:e,trigger:l=$,valuePropName:o=ee,props:a}=t,s=Object.prototype.hasOwnProperty.call(a,o),[c,i]=x(n),d=s?a[o]:c,u=p(()=>a[l],[a,l]),m=_(f=>{const g=typeof f=="function"?f(d):f;e&&e(g,d)===!1||(s||i(g),u&&u(g))},[s,e,d,u]);return[d,m]}function ne(t,...n){try{const e=t(...n);return e instanceof Promise?e:Promise.resolve(e)}catch(e){return Promise.reject(e)}}const I=typeof Promise.try=="function"?Promise.try:ne;function re(t,n){let e=t;const l=[],o=()=>e,a=s=>{const c=e;e=s,l.forEach(i=>i(e)),n&&I(n,e,c).catch(i=>{console.error("Error in external state side effect, Please do it within side effects:",i)})};return{get:o,set:a,use:()=>{const[s,c]=r.useState(e);return r.useEffect(()=>(l.push(c),()=>{const i=l.indexOf(c);i>-1&&l.splice(i,1)}),[]),[s,a]}}}function le(t,n){const e=n||new Date,l=e.getFullYear(),o=e.getMonth()+1,a=e.getDate(),s=e.getHours(),c=e.getMinutes(),i=e.getSeconds(),d=e.getMilliseconds(),u=e.getDay(),m=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],f=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],g=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],A=["January","February","March","April","May","June","July","August","September","October","November","December"],Y=f[u],D=m[u],T=o-1,P=A[T],k=g[T],O={YY:l.toString().slice(2),YYYY:l.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:k,MMMM:P,D:a.toString(),DD:a.toString().padStart(2,"0"),d:u.toString(),dd:D,ddd:D,dddd:Y,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:i.toString(),ss:i.toString().padStart(2,"0"),SSS:d.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return t.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,j=>O[j])}class ae{count=0;next(){return this.count++}}export{U as ArrayRender,ae as Counter,X as DateRender,Z as False,h as If,L as Pipe,G as Scope,q as SizeBox,b as Styles,y as Switch,Q as Toggle,V as True,z as When,C as childrenLoop,re as createExternalState,M as cx,le as formatDate,I as safePromiseTry,te as useControlled};
1
+ import r,{useMemo as p,Children as J,Fragment as S,isValidElement as _,cloneElement as R,useEffect as W,useState as x,useCallback as H}from"react";function C(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const l of t)if(n(l,e++)===!1)break}else n(t,e)}const B=(t,n)=>t===n,E=t=>r.createElement(r.Fragment,null,t.children);E.displayName="Switch_Case";const w=t=>r.createElement(r.Fragment,null,t.children);w.displayName="Switch_Default";const y=t=>{const{value:n,compare:e=B,children:l,strict:o=!1}=t,a=new Set;let s=null,c=null,i=!1;return C(l,(d,u)=>{if(!r.isValidElement(d))throw new Error(`Switch Children only accepts valid React elements at index ${u}`);const m=d.type;if(m.displayName===E.displayName){const f=d.props;if(a.has(f.value))throw new Error(`Switch found duplicate Case value at index ${u}: ${JSON.stringify(f.value)}${o?" (detected in strict mode)":""}`);if(a.add(f.value),!s&&e(n,f.value)&&(s=f.children,o===!1))return!1}else if(m.displayName===w.displayName){if(i)throw new Error(`Switch can only have one Default child at index ${u}`);if(i=!0,c=d.props.children,!o&&s)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(m.displayName||m.name||m)} at index ${u}`)}),r.createElement(r.Fragment,null,s??c)};y.displayName="Switch",y.Case=E,y.Default=w,y.createTyped=function(){return{Switch:y,Case:E,Default:w}};const N=t=>r.createElement(r.Fragment,null,t.children),v=({children:t})=>r.createElement(r.Fragment,null,t),F=t=>r.createElement(r.Fragment,null,t.children);N.displayName="If_Then",v.displayName="If_Else",F.displayName="If_ElseIf";const h=({condition:t,children:n})=>{let e=null,l=null;const o=[];if(r.Children.forEach(n,a=>{if(!r.isValidElement(a))throw new Error("If component only accepts valid React elements");const s=a.type;if(s.displayName===N.displayName){if(e)throw new Error("If component can only have one Then child");e=a}else if(s.displayName===F.displayName)o.push(a);else if(s.displayName===v.displayName){if(l)throw new Error("If component can only have one Else child");l=a}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(s.displayName||s.name||s)}`)}),t)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const a of o)if(a.props.condition)return r.createElement(r.Fragment,null,a.props.children);return l?r.createElement(r.Fragment,null,l.props.children):null};h.displayName="If",h.Then=N,h.ElseIf=F,h.Else=v,h.createTyped=function(){return{If:h,Then:N,ElseIf:F,Else:v}};const V=({condition:t,children:n})=>t?r.createElement(r.Fragment,null,n):null,Z=({condition:t,children:n})=>t===!1?r.createElement(r.Fragment,null,n):null,z=({all:t,any:n,none:e,children:l,fallback:o})=>p(()=>(t&&(n||e)&&console.warn('When: Multiple condition types (all, any, none) provided; "all" takes precedence.'),!!(t&&t.length>0&&t.every(Boolean)||n&&n.length>0&&n.some(Boolean)||e&&e.length>0&&e.every(a=>!a))),[t,n,e])?r.createElement(r.Fragment,null,l):r.createElement(r.Fragment,null,o||null),L=({data:t,transform:n,render:e,fallback:l})=>{const o=p(()=>n.reduce((a,s)=>s(a),t),[t,n]);return o==null?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))},q=t=>{const{children:n,h:e,w:l,size:o,height:a,width:s,className:c}=t;return r.createElement("div",{style:{width:o||l||s,height:o||e||a,flexShrink:0},className:c},n)},G=({let:t,props:n,children:e,fallback:l})=>{const o=p(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?r.createElement(r.Fragment,null,l||null):r.createElement(r.Fragment,null,e(o))};function M(...t){const n=new Set;for(const e of t)if(e){if(typeof e=="string")n.add(e);else if(Array.isArray(e))e.forEach(l=>n.add(l));else if(typeof e=="object")for(const[l,o]of Object.entries(e))o&&n.add(l)}return Array.from(n).join(" ")}const K=t=>typeof t=="object"&&!!t,b=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(J.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),r.createElement(S,null,n);if(!t)return r.createElement(S,null,n);const l=typeof t=="string"?t:M(...Object.values(t));if(e)return r.createElement(e===!0?"div":e,{className:l},n);if(_(n)){const o=n;let a=o?.props?.className;return o?.type?.displayName===b.displayName&&K(a)&&(a=M(...Object.values(a))),R(n,{className:M(l,a)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),r.createElement(S,null,n)};b.displayName="W/Styles";const Q=t=>{const{index:n=0,options:e,next:l,render:o}=t;W(()=>{if(e.length<n+1)throw new Error(`Index ${n} is out of bounds for options array of length ${e.length}. Defaulting to first option.`)},[n,e]);const[a,s]=x(n),c=()=>{s(i=>e.length?l?l(i,e):(i+1)%e.length:i)};return o(e[a],c)};function U(t){const{items:n,renderItem:e,filter:l}=t;return n?r.createElement(S,null,n.map((o,a)=>l&&!l(o)?null:e(o,a))):(console.error("ArrayRender: items is null"),null)}function X({source:t,format:n,children:e}){const l=p(()=>{if(t instanceof Date)return t;if(typeof t=="string"||typeof t=="number"){const a=new Date(t);return isNaN(a.getTime())?null:a}return null},[t]),o=p(()=>l?n?n(l):l.toLocaleString():null,[l,n]);return!o||!e?null:r.createElement(r.Fragment,null,e(o))}const $="onChange",ee="value";function te(t){const{defaultValue:n,onBeforeChange:e,trigger:l=$,valuePropName:o=ee,props:a}=t,s=Object.prototype.hasOwnProperty.call(a,o),[c,i]=x(n),d=s?a[o]:c,u=p(()=>a[l],[a,l]),m=H(f=>{const g=typeof f=="function"?f(d):f;e&&e(g,d)===!1||(s||i(g),u&&u(g))},[s,e,d,u]);return[d,m]}function ne(t,...n){try{const e=t(...n);return e instanceof Promise?e:Promise.resolve(e)}catch(e){return Promise.reject(e)}}const I=typeof Promise.try=="function"?Promise.try.bind(Promise):ne;function re(t,n){let e=t;const l=[],o=()=>e,a=s=>{const c=e;e=s,l.forEach(i=>i(e)),n&&I(n,e,c).catch(i=>{console.error("Error in external state side effect, Please do it within side effects:",i)})};return{get:o,set:a,use:()=>{const[s,c]=r.useState(e);return r.useEffect(()=>(l.push(c),()=>{const i=l.indexOf(c);i>-1&&l.splice(i,1)}),[]),[s,a]},__listeners:l}}function le(t,n){const e=n||new Date,l=e.getFullYear(),o=e.getMonth()+1,a=e.getDate(),s=e.getHours(),c=e.getMinutes(),i=e.getSeconds(),d=e.getMilliseconds(),u=e.getDay(),m=["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],f=["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],g=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],A=["January","February","March","April","May","June","July","August","September","October","November","December"],P=f[u],D=m[u],T=o-1,Y=A[T],k=g[T],O={YY:l.toString().slice(2),YYYY:l.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:k,MMMM:Y,D:a.toString(),DD:a.toString().padStart(2,"0"),d:u.toString(),dd:D,ddd:D,dddd:P,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:i.toString(),ss:i.toString().padStart(2,"0"),SSS:d.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:s<12?"AM":"PM",a:s<12?"am":"pm"};return t.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,j=>O[j])}class ae{count=0;next(){return this.count++}}export{U as ArrayRender,ae as Counter,X as DateRender,Z as False,h as If,L as Pipe,G as Scope,q as SizeBox,b as Styles,y as Switch,Q as Toggle,V as True,z as When,C as childrenLoop,re as createExternalState,M as cx,le as formatDate,I as safePromiseTry,te as useControlled};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wwog/react",
3
- "version": "1.2.10",
3
+ "version": "1.2.11",
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",
@@ -24,7 +24,7 @@
24
24
  "build": "unbuild",
25
25
  "format": "biome format --write src",
26
26
  "check": "biome check --write src",
27
- "test:unit": "vitest run",
27
+ "test:unit": "vitest run --browser.headless",
28
28
  "test:types": "tsc --noEmit --skipLibCheck",
29
29
  "all-suites": "pnpm run format && pnpm run check && pnpm run test:types && pnpm run test:unit",
30
30
  "test:watch": "vitest"
@@ -0,0 +1,184 @@
1
+ import { expect, describe, it, vi } from "vitest";
2
+ import { render } from "vitest-browser-react";
3
+ import {
4
+ createExternalState,
5
+ type ExternalWithKernel,
6
+ } from "./createExternalState";
7
+ import { safePromiseTry } from "./promise";
8
+ import React from "react";
9
+
10
+ describe("createExternalState", () => {
11
+ it("测试初始状态值", () => {
12
+ const initialState = "initial";
13
+ const state = createExternalState(initialState);
14
+ expect(state.get()).toBe(initialState);
15
+ });
16
+
17
+ it("测试set方法更新状态", () => {
18
+ const initialState = "initial";
19
+ const newState = "updated";
20
+ const state = createExternalState(initialState);
21
+
22
+ state.set(newState);
23
+ expect(state.get()).toBe(newState);
24
+ });
25
+
26
+ it("测试use钩子在组件中使用", async () => {
27
+ const initialState = "initial";
28
+ const state = createExternalState(initialState);
29
+
30
+ function TestComponent() {
31
+ const [value, setValue] = state.use();
32
+ return (
33
+ <div>
34
+ <span data-testid="value">{value}</span>
35
+ <button onClick={() => setValue("updated")}>Update</button>
36
+ </div>
37
+ );
38
+ }
39
+
40
+ const { getByTestId, getByText } = render(<TestComponent />);
41
+ const valueLocator = getByTestId("value");
42
+ const buttonLocator = getByText("Update");
43
+ expect(valueLocator.element().textContent).toBe(initialState);
44
+ await buttonLocator.click();
45
+ expect(valueLocator.element().textContent).toBe("updated");
46
+ expect(state.get()).toBe("updated");
47
+ });
48
+
49
+ it("测试多个组件共享状态", async () => {
50
+ const initialState = "initial";
51
+ const state = createExternalState(initialState);
52
+
53
+ function ComponentA() {
54
+ const [value, setValue] = state.use();
55
+ return (
56
+ <div>
57
+ <span data-testid="valueA">{value}</span>
58
+ <button data-testid="buttonA" onClick={() => setValue("updatedA")}>
59
+ Update A
60
+ </button>
61
+ </div>
62
+ );
63
+ }
64
+
65
+ function ComponentB() {
66
+ const [value, setValue] = state.use();
67
+ return (
68
+ <div>
69
+ <span data-testid="valueB">{value}</span>
70
+ <button data-testid="buttonB" onClick={() => setValue("updatedB")}>
71
+ Update B
72
+ </button>
73
+ </div>
74
+ );
75
+ }
76
+
77
+ const { getByTestId } = render(
78
+ <>
79
+ <ComponentA />
80
+ <ComponentB />
81
+ </>
82
+ );
83
+ const valueALocator = getByTestId("valueA");
84
+ const valueBLocator = getByTestId("valueB");
85
+ const buttonALocator = getByTestId("buttonA");
86
+ const buttonBLocator = getByTestId("buttonB");
87
+ expect(valueALocator.element().textContent).toBe(initialState);
88
+ expect(valueBLocator.element().textContent).toBe(initialState);
89
+ await buttonALocator.click();
90
+ expect(valueALocator.element().textContent).toBe("updatedA");
91
+ expect(valueBLocator.element().textContent).toBe("updatedA");
92
+ expect(state.get()).toBe("updatedA");
93
+ await buttonBLocator.click();
94
+ expect(valueALocator.element().textContent).toBe("updatedB");
95
+ expect(valueBLocator.element().textContent).toBe("updatedB");
96
+ expect(state.get()).toBe("updatedB");
97
+ });
98
+
99
+ it("测试组件卸载时移除监听器", () => {
100
+ const initialState = "initial";
101
+ const state = createExternalState(
102
+ initialState
103
+ ) as ExternalWithKernel<string>;
104
+
105
+ function TestComponent() {
106
+ const [value, setValue] = state.use();
107
+ return (
108
+ <div>
109
+ <span data-testid="value">{value}</span>
110
+ <button onClick={() => setValue("updated")}>Update</button>
111
+ </div>
112
+ );
113
+ }
114
+ expect(state.__listeners.length).toBe(0);
115
+ const { rerender, getByTestId } = render(<TestComponent />);
116
+ expect(state.__listeners.length).toBe(1);
117
+ const valueLocator = getByTestId("value");
118
+ expect(valueLocator.element().textContent).toBe(initialState);
119
+ rerender(<div>Rerender</div>);
120
+ expect(state.__listeners.length).toBe(0);
121
+ });
122
+
123
+ it("测试副作用函数", () => {
124
+ const mockSideEffect = vi.fn((...args) => void 0);
125
+ const initialState: string = "initial";
126
+ const state = createExternalState(initialState, mockSideEffect);
127
+ state.set("updated");
128
+ expect(mockSideEffect).toHaveBeenCalledTimes(1);
129
+ expect(mockSideEffect).toHaveBeenCalledWith("updated", initialState);
130
+ state.set("updated2");
131
+ expect(mockSideEffect).toHaveBeenCalledTimes(2);
132
+ expect(mockSideEffect).toHaveBeenCalledWith("updated2", "updated");
133
+ });
134
+
135
+ it("测试异步副作用函数", async () => {
136
+ const mockAsyncSideEffect = vi.fn().mockResolvedValue(undefined);
137
+ const initialState: string = "initial";
138
+ const state = createExternalState(initialState, mockAsyncSideEffect);
139
+
140
+ state.set("updated");
141
+ expect(mockAsyncSideEffect).toHaveBeenCalledTimes(1);
142
+ expect(mockAsyncSideEffect).toHaveBeenCalledWith("updated", initialState);
143
+ });
144
+
145
+ it("测试复杂数据类型", async () => {
146
+ interface User {
147
+ name: string;
148
+ age: number;
149
+ }
150
+
151
+ const initialUser: User = { name: "张三", age: 25 };
152
+ const state = createExternalState<User>(initialUser);
153
+
154
+ expect(state.get()).toEqual(initialUser);
155
+
156
+ const updatedUser: User = { name: "李四", age: 30 };
157
+ state.set(updatedUser);
158
+ expect(state.get()).toEqual(updatedUser);
159
+
160
+ function TestComponent() {
161
+ const [user, setUser] = state.use();
162
+ return (
163
+ <div>
164
+ <span data-testid="name">{user.name}</span>
165
+ <span data-testid="age">{user.age}</span>
166
+ <button onClick={() => setUser({ name: "王五", age: 35 })}>
167
+ Update
168
+ </button>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ const { getByTestId, getByText } = render(<TestComponent />);
174
+ const nameLocator = getByTestId("name");
175
+ const ageLocator = getByTestId("age");
176
+ const buttonLocator = getByText("Update");
177
+ expect(nameLocator.element().textContent).toBe("李四");
178
+ expect(ageLocator.element().textContent).toBe("30");
179
+ await buttonLocator.click();
180
+ expect(nameLocator.element().textContent).toBe("王五");
181
+ expect(ageLocator.element().textContent).toBe("35");
182
+ expect(state.get()).toEqual({ name: "王五", age: 35 });
183
+ });
184
+ });
@@ -18,7 +18,7 @@ export type CreateStateListener<T> = (state: T) => void;
18
18
  export type ExternalSideEffect<T> = (
19
19
  newState: T,
20
20
  prevState: T
21
- ) => void | Promise<void>;
21
+ ) => any | Promise<any>;
22
22
 
23
23
  /**
24
24
  * @en External state management interface
@@ -48,6 +48,10 @@ export interface ExternalState<T> {
48
48
  use: () => [T, (newState: T) => void];
49
49
  }
50
50
 
51
+ export interface ExternalWithKernel<T> extends ExternalState<T> {
52
+ __listeners: CreateStateListener<T>[];
53
+ }
54
+
51
55
  /**
52
56
  *
53
57
  * @example
@@ -89,7 +93,10 @@ export function createExternalState<T>(
89
93
  listeners.forEach((listener) => listener(state));
90
94
  if (sideEffect) {
91
95
  safePromiseTry(sideEffect, state, prevState).catch((error) => {
92
- console.error("Error in external state side effect, Please do it within side effects:", error);
96
+ console.error(
97
+ "Error in external state side effect, Please do it within side effects:",
98
+ error
99
+ );
93
100
  });
94
101
  }
95
102
  };
@@ -110,5 +117,6 @@ export function createExternalState<T>(
110
117
  return [localState, set] as [T, (newState: T) => void];
111
118
  };
112
119
 
113
- return { get, set, use };
120
+ //@ts-expect-error ignore
121
+ return { get, set, use, __listeners: listeners };
114
122
  }
@@ -31,7 +31,7 @@ function promiseTry<T, U extends unknown[]>(
31
31
 
32
32
  export const safePromiseTry = (() => {
33
33
  if (typeof Promise.try === "function") {
34
- return Promise.try;
34
+ return Promise.try.bind(Promise);
35
35
  } else {
36
36
  return promiseTry;
37
37
  }