@wwog/react 1.2.20 → 1.2.21
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 -6
- package/dist/index.d.mts +59 -16
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/src/utils/createExternalState.test.tsx +39 -2
- package/src/utils/createExternalState.ts +87 -22
package/README.md
CHANGED
|
@@ -393,14 +393,19 @@ You can also use a container wrapper element:
|
|
|
393
393
|
|
|
394
394
|
#### `createExternalState` (v1.2.9+, useGetter added in v1.2.13)
|
|
395
395
|
|
|
396
|
-
|
|
396
|
+
> v1.2.21: Refactor the API to move sideeffects into options and enhance support for the transform interface
|
|
397
|
+
> v1.2.13: add useGetter
|
|
398
|
+
|
|
399
|
+
> 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.
|
|
397
400
|
|
|
398
401
|
```tsx
|
|
399
402
|
import { createExternalState } from "@wwog/react";
|
|
400
403
|
|
|
401
404
|
// Create a global theme state
|
|
402
|
-
const themeState = createExternalState("light",
|
|
403
|
-
|
|
405
|
+
const themeState = createExternalState("light", {
|
|
406
|
+
sideEffect: (newTheme, oldTheme) => {
|
|
407
|
+
console.log(`Theme changed from ${oldTheme} to ${newTheme}`);
|
|
408
|
+
},
|
|
404
409
|
});
|
|
405
410
|
|
|
406
411
|
// Get or modify state from anywhere
|
|
@@ -429,15 +434,17 @@ function ReadOnlyThemeConsumer() {
|
|
|
429
434
|
}
|
|
430
435
|
```
|
|
431
436
|
|
|
432
|
-
- `createExternalState<T>(initialState,
|
|
437
|
+
- `createExternalState<T>(initialState, options?)`: Creates a state accessible outside components
|
|
433
438
|
- `initialState`: Initial state value
|
|
434
|
-
- `sideEffect`: Optional side effect function, called on state updates
|
|
439
|
+
- `options.sideEffect`: Optional side effect function, called on state updates
|
|
435
440
|
- Returns an object with methods:
|
|
436
441
|
- `get()`: Get the current state value
|
|
437
442
|
- `set(newState)`: Update the state value
|
|
438
443
|
- `use()`: React Hook, returns `[state, setState]` for using this state in components
|
|
439
444
|
- `useGetter()`: React Hook that only returns the state value, useful when you only need to read the state
|
|
440
|
-
|
|
445
|
+
- `options.transform`:
|
|
446
|
+
- `get`
|
|
447
|
+
- `set`
|
|
441
448
|
Use cases:
|
|
442
449
|
|
|
443
450
|
- Global state management (themes, user settings, etc.)
|
package/dist/index.d.mts
CHANGED
|
@@ -492,49 +492,92 @@ type CreateStateListener<T> = (state: T) => void;
|
|
|
492
492
|
* @param prevState The previous state value / 之前的状态值
|
|
493
493
|
*/
|
|
494
494
|
type ExternalSideEffect<T> = (newState: T, prevState: T) => any | Promise<any>;
|
|
495
|
+
/**
|
|
496
|
+
* @en Transform functions for getting and setting state
|
|
497
|
+
* @zh 用于获取和设置状态的转换函数
|
|
498
|
+
* @template T The type of the state / 状态的类型
|
|
499
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
500
|
+
*/
|
|
501
|
+
interface Transform<T, U = T> {
|
|
502
|
+
/**
|
|
503
|
+
* @en Transform function for getting state
|
|
504
|
+
* @zh 获取状态时的转换函数
|
|
505
|
+
*/
|
|
506
|
+
get?: (state: T) => U;
|
|
507
|
+
/**
|
|
508
|
+
* @en Transform function for setting state
|
|
509
|
+
* @zh 设置状态时的转换函数
|
|
510
|
+
*/
|
|
511
|
+
set?: (value: U) => T;
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* @en Options for creating external state
|
|
515
|
+
* @zh 创建外部状态的选项
|
|
516
|
+
* @template T The type of the state / 状态的类型
|
|
517
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
518
|
+
*/
|
|
519
|
+
interface ExternalStateOptions<T, U = T> {
|
|
520
|
+
/**
|
|
521
|
+
* @en Side effect function to run after state changes
|
|
522
|
+
* @zh 状态变更后运行的副作用函数
|
|
523
|
+
*/
|
|
524
|
+
sideEffect?: ExternalSideEffect<T>;
|
|
525
|
+
/**
|
|
526
|
+
* @en Transform functions for getting and setting state
|
|
527
|
+
* @zh 用于获取和设置状态的转换函数
|
|
528
|
+
*/
|
|
529
|
+
transform?: Transform<T, U>;
|
|
530
|
+
}
|
|
495
531
|
/**
|
|
496
532
|
* @en External state management interface
|
|
497
533
|
* @zh 外部状态管理接口
|
|
498
534
|
* @template T The type of the state / 状态的类型
|
|
535
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
499
536
|
*/
|
|
500
|
-
interface ExternalState<T> {
|
|
537
|
+
interface ExternalState<T, U = T> {
|
|
501
538
|
/**
|
|
502
539
|
* @en Get the current state value
|
|
503
540
|
* @zh 获取当前状态值
|
|
504
|
-
* @returns The current state value /
|
|
541
|
+
* @returns The current state value (transformed if transform.get is provided) / 当前状态值(如果提供了 transform.get 则进行转换)
|
|
505
542
|
*/
|
|
506
|
-
get: () =>
|
|
543
|
+
get: () => U;
|
|
507
544
|
/**
|
|
508
545
|
* @en Set a new state value
|
|
509
546
|
* @zh 设置新的状态值
|
|
510
|
-
* @param newState The new state value /
|
|
547
|
+
* @param newState The new state value or a function that returns it / 新的状态值或返回新状态的函数
|
|
511
548
|
*/
|
|
512
|
-
set: (newState:
|
|
549
|
+
set: (newState: U | ((prevState: U) => U)) => void;
|
|
513
550
|
/**
|
|
514
551
|
* @en React Hook for using external state in components
|
|
515
552
|
* @zh 在组件中使用外部状态的 React Hook
|
|
516
|
-
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
553
|
+
* @returns Array containing current钣金龙8国际唯一官网 current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
517
554
|
*/
|
|
518
|
-
use: () => [
|
|
555
|
+
use: () => [U, (newState: U | ((prevState: U) => U)) => void];
|
|
519
556
|
/**
|
|
520
557
|
* @zh use的变体,只获取value.
|
|
521
558
|
* @en A variant of use that only gets the value.
|
|
522
559
|
*/
|
|
523
|
-
useGetter: () =>
|
|
560
|
+
useGetter: () => U;
|
|
524
561
|
}
|
|
525
|
-
interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
562
|
+
interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
526
563
|
__listeners: CreateStateListener<T>[];
|
|
527
564
|
}
|
|
528
565
|
/**
|
|
529
566
|
*
|
|
530
567
|
* @example
|
|
531
568
|
* ```tsx
|
|
532
|
-
* // Create an app-level theme state
|
|
533
|
-
* const themeState = createExternalState('light'
|
|
569
|
+
* // Create an app-level theme state with options
|
|
570
|
+
* const themeState = createExternalState('light', {
|
|
571
|
+
* sideEffect: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
|
|
572
|
+
* transform: {
|
|
573
|
+
* get: (state) => state.toUpperCase(),
|
|
574
|
+
* set: (value) => value.toLowerCase()
|
|
575
|
+
* }
|
|
576
|
+
* });
|
|
534
577
|
*
|
|
535
578
|
* // Get or modify state outside components
|
|
536
|
-
* console.log(themeState.get()); // '
|
|
537
|
-
* themeState.set('dark');
|
|
579
|
+
* console.log(themeState.get()); // 'LIGHT'
|
|
580
|
+
* themeState.set((prev) => prev === 'light' ? 'dark' : 'light'); // Toggle theme
|
|
538
581
|
*
|
|
539
582
|
* // Use state in components
|
|
540
583
|
* function ThemeConsumer() {
|
|
@@ -542,7 +585,7 @@ interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
542
585
|
*
|
|
543
586
|
* return (
|
|
544
587
|
* <div className={theme}>
|
|
545
|
-
* <button onClick={() => setTheme(
|
|
588
|
+
* <button onClick={() => setTheme((prev) => prev === 'LIGHT' ? 'DARK' : 'LIGHT')}>
|
|
546
589
|
* Toggle theme / 切换主题
|
|
547
590
|
* </button>
|
|
548
591
|
* </div>
|
|
@@ -550,7 +593,7 @@ interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
550
593
|
* }
|
|
551
594
|
* ```
|
|
552
595
|
*/
|
|
553
|
-
declare function createExternalState<T>(initialState: T | (() => T),
|
|
596
|
+
declare function createExternalState<T, U = T>(initialState: T | (() => T), options?: ExternalStateOptions<T, U>): ExternalState<T, U>;
|
|
554
597
|
|
|
555
598
|
/**
|
|
556
599
|
* @description 性能优化,替代 React.Children.forEach, 回调可以返回 false 来中断循环
|
|
@@ -600,4 +643,4 @@ declare class Counter {
|
|
|
600
643
|
declare const safePromiseTry: <T, U extends unknown[]>(callbackFn: (...args: U) => T | PromiseLike<T>, ...args: U) => Promise<Awaited<T>>;
|
|
601
644
|
|
|
602
645
|
export { ArrayRender, Clamp, Counter, DateRender, False, If, Pipe, Scope, SizeBox, Styles, Switch, Toggle, True, When, childrenLoop, createExternalState, cx, formatDate, safePromiseTry, useControlled };
|
|
603
|
-
export type { ArrayRenderProps, ClampProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalWithKernel, FalseProps, IfProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, TrueProps, UseControlledOptions, WhenProps };
|
|
646
|
+
export type { ArrayRenderProps, ClampProps, CreateStateListener, CxInput, DateRenderProps, ElseIfProps, ElseProps, ExternalSideEffect, ExternalState, ExternalStateOptions, ExternalWithKernel, FalseProps, IfProps, PipeProps, ScopeProps, StylesDescriptor, StylesProps, StylesType, SwitchCaseProps, SwitchDefaultProps, SwitchProps, ThenProps, ToggleProps, Transform, TrueProps, UseControlledOptions, WhenProps };
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r,{useMemo as y,Children as B,Fragment as w,isValidElement as W,cloneElement as J,useEffect as R,useState as
|
|
1
|
+
import r,{useMemo as y,Children as B,Fragment as w,isValidElement as W,cloneElement as J,useEffect as R,useState as I,useRef as _,useLayoutEffect as L,useCallback as Z}from"react";function P(t,n){if(t===void 0)return;let e=0;if(Array.isArray(t)){for(const a of t)if(n(a,e++)===!1)break}else n(t,e)}const z=(t,n)=>t===n,v=t=>r.createElement(r.Fragment,null,t.children);v.displayName="Switch_Case";const N=t=>r.createElement(r.Fragment,null,t.children);N.displayName="Switch_Default";const S=t=>{const{value:n,compare:e=z,children:a,strict:o=!1}=t,l=new Set;let i=null,f=null,m=!1;return P(a,(s,u)=>{if(!r.isValidElement(s))throw new Error(`Switch Children only accepts valid React elements at index ${u}`);const c=s.type;if(c.displayName===v.displayName){const d=s.props;if(l.has(d.value))throw new Error(`Switch found duplicate Case value at index ${u}: ${JSON.stringify(d.value)}${o?" (detected in strict mode)":""}`);if(l.add(d.value),!i&&e(n,d.value)&&(i=d.children,o===!1))return!1}else if(c.displayName===N.displayName){if(m)throw new Error(`Switch can only have one Default child at index ${u}`);if(m=!0,f=s.props.children,!o&&i)return!1}else throw new Error(`Switch Children only accepts 'Case' or 'Default' elements, found: ${String(c.displayName||c.name||c)} at index ${u}`)}),r.createElement(r.Fragment,null,i??f)};S.displayName="Switch",S.Case=v,S.Default=N,S.createTyped=function(){return{Switch:S,Case:v,Default:N}};const b=t=>r.createElement(r.Fragment,null,t.children),M=({children:t})=>r.createElement(r.Fragment,null,t),F=t=>r.createElement(r.Fragment,null,t.children);b.displayName="If_Then",M.displayName="If_Else",F.displayName="If_ElseIf";const g=({condition:t,children:n})=>{let e=null,a=null;const o=[];if(r.Children.forEach(n,l=>{if(!r.isValidElement(l))throw new Error("If component only accepts valid React elements");const i=l.type;if(i.displayName===b.displayName){if(e)throw new Error("If component can only have one Then child");e=l}else if(i.displayName===F.displayName)o.push(l);else if(i.displayName===M.displayName){if(a)throw new Error("If component can only have one Else child");a=l}else throw new Error(`If component only accepts 'Then', 'ElseIf', or 'Else' elements as children, found: ${String(i.displayName||i.name||i)}`)}),t)return e?r.createElement(r.Fragment,null,e.props.children):null;for(const l of o)if(l.props.condition)return r.createElement(r.Fragment,null,l.props.children);return a?r.createElement(r.Fragment,null,a.props.children):null};g.displayName="If",g.Then=b,g.ElseIf=F,g.Else=M,g.createTyped=function(){return{If:g,Then:b,ElseIf:F,Else:M}};const T=({condition:t,children:n})=>t?r.createElement(r.Fragment,null,n):null,V=({condition:t,children:n})=>t===!1?r.createElement(r.Fragment,null,n):null,G=({all:t,any:n,none:e,children:a,fallback:o})=>y(()=>(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(l=>!l))),[t,n,e])?r.createElement(r.Fragment,null,a):r.createElement(r.Fragment,null,o||null),U=({data:t,transform:n,render:e,fallback:a})=>{const o=y(()=>n.reduce((l,i)=>i(l),t),[t,n]);return o==null?r.createElement(r.Fragment,null,a||null):r.createElement(r.Fragment,null,e(o))},q=t=>{const{children:n,h:e,w:a,size:o,height:l,width:i,className:f}=t;return r.createElement("div",{style:{width:o||a||i,height:o||e||l,flexShrink:0},className:f},n)},K=({let:t,props:n,children:e,fallback:a})=>{const o=y(()=>typeof t=="function"?t(n):t,[t,n]);return!e||!Object.keys(o).length?r.createElement(r.Fragment,null,a||null):r.createElement(r.Fragment,null,e(o))};function x(...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(a=>n.add(a));else if(typeof e=="object")for(const[a,o]of Object.entries(e))o&&n.add(a)}return Array.from(n).join(" ")}const Q=t=>typeof t=="object"&&!!t,A=({className:t,children:n,asWrapper:e=!1})=>{if(!n)return null;if(B.count(n)>1)return console.error("<Styles>: children has more than one child. Please check your code."),r.createElement(w,null,n);if(!t)return r.createElement(w,null,n);const a=typeof t=="string"?t:x(...Object.values(t));if(e)return r.createElement(e===!0?"div":e,{className:a},n);if(W(n)){const o=n;let l=o?.props?.className;return o?.type?.displayName===A.displayName&&Q(l)&&(l=x(...Object.values(l))),J(n,{className:x(a,l)})}return console.error("<Styles>: children is not a valid React element. Please check your code."),r.createElement(w,null,n)};A.displayName="W/Styles";const X=t=>{const{index:n=0,options:e,next:a,render:o}=t;R(()=>{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[l,i]=I(n),f=()=>{i(m=>e.length?a?a(m,e):(m+1)%e.length:m)};return o(e[l],f)},$=t=>{const{maxLine:n=1,text:e,extraHeight:a=22,extraContent:o,wrapperStyle:l}=t,i=_(null),[f,m]=I(!1),s=y(()=>!(e==null||e===""),[e]),u=()=>{if(!i.current)return;const c=document.createElement("div");c.textContent=e;const d=getComputedStyle(i.current);if(c.style.width=d.width,c.style.fontSize=d.fontSize,c.style.lineHeight=d.lineHeight,c.style.wordBreak=d.wordBreak,c.style.visibility="hidden",l){const E=Object.keys(l).map(p=>p.replace(/[A-Z]/g,k=>`-${k.toLowerCase()}`));for(const p of E)c.style[p]=d[p]}document.body.appendChild(c);const h=parseInt(getComputedStyle(c).lineHeight)||20,C=c.offsetHeight,D=Math.round(C/h);document.body.removeChild(c),m(D>n)};return L(()=>{s&&u()},[n,e,s]),r.createElement(T,{condition:s},r.createElement("div",{ref:i,style:{overflow:"hidden",width:"100%",display:"flex",...l}},r.createElement("div",{style:{display:"-webkit-box",WebkitBoxOrient:"vertical",WebkitLineClamp:n,overflow:"hidden",wordBreak:"break-all"}},r.createElement(T,{condition:f},r.createElement("div",{style:{float:"right",height:"100%",marginBottom:-a}}),r.createElement("div",{style:{float:"right",clear:"both",height:a}},o)),e)))};function ee(t){const{items:n,renderItem:e,filter:a}=t;return n?r.createElement(w,null,n.map((o,l)=>a&&!a(o)?null:e(o,l))):(console.error("ArrayRender: items is null"),null)}function te({source:t,format:n,children:e}){const a=y(()=>{if(t instanceof Date)return t;if(typeof t=="string"||typeof t=="number"){const l=new Date(t);return isNaN(l.getTime())?null:l}return null},[t]),o=y(()=>a?n?n(a):a.toLocaleString():null,[a,n]);return!o||!e?null:r.createElement(r.Fragment,null,e(o))}const ne="onChange",re="value";function le(t){const{defaultValue:n,onBeforeChange:e,trigger:a=ne,valuePropName:o=re,props:l}=t,i=Object.prototype.hasOwnProperty.call(l,o),[f,m]=I(n),s=i?l[o]:f,u=y(()=>l[a],[l,a]),c=Z(d=>{const h=typeof d=="function"?d(s):d;e&&e(h,s)===!1||(i||m(h),u&&u(h))},[i,e,s,u]);return[s,c]}function ae(t,...n){try{const e=t(...n);return e instanceof Promise?e:Promise.resolve(e)}catch(e){return Promise.reject(e)}}const Y=typeof Promise.try=="function"?Promise.try.bind(Promise):ae;function oe(t,n={}){let e=typeof t=="function"?t():t;const a=[],{sideEffect:o,transform:l}=n,i=()=>{const s=e;return l?.get?l.get(s):s},f=s=>{const u=e,c=l?.get?l.get(u):u;e=l?.set?l.set(typeof s=="function"?s(c):s):typeof s=="function"?s(c):s,a.forEach(d=>d(e)),o&&Y(o,e,u).catch(d=>{console.error("Error in external state side effect, Please do it within side effects:",d)})},m=()=>{const[s,u]=r.useState(e);return r.useEffect(()=>(a.push(u),()=>{const c=a.indexOf(u);c>-1&&a.splice(c,1)}),[]),[l?.get?l.get(s):s,f]};return{get:i,set:f,use:m,useGetter:()=>{const[s]=m();return s},__listeners:a}}function ie(t,n){const e=n||new Date,a=e.getFullYear(),o=e.getMonth()+1,l=e.getDate(),i=e.getHours(),f=e.getMinutes(),m=e.getSeconds(),s=e.getMilliseconds(),u=e.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"],C=["January","February","March","April","May","June","July","August","September","October","November","December"],D=d[u],E=c[u],p=o-1,k=C[p],O=h[p],j={YY:a.toString().slice(2),YYYY:a.toString(),M:o.toString(),MM:o.toString().padStart(2,"0"),MMM:O,MMMM:k,D:l.toString(),DD:l.toString().padStart(2,"0"),d:u.toString(),dd:E,ddd:E,dddd:D,H:i.toString(),HH:i.toString().padStart(2,"0"),h:(i%12).toString(),hh:(i%12).toString().padStart(2,"0"),m:f.toString(),mm:f.toString().padStart(2,"0"),s:m.toString(),ss:m.toString().padStart(2,"0"),SSS:s.toString().padStart(3,"0"),Z:"+08:00",ZZ:"+0800",A:i<12?"AM":"PM",a:i<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,H=>j[H])}class se{count=0;next(){return this.count++}}export{ee as ArrayRender,$ as Clamp,se as Counter,te as DateRender,V as False,g as If,U as Pipe,K as Scope,q as SizeBox,A as Styles,S as Switch,X as Toggle,T as True,G as When,P as childrenLoop,oe as createExternalState,x as cx,ie as formatDate,Y as safePromiseTry,le as useControlled};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wwog/react",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.21",
|
|
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",
|
|
@@ -123,7 +123,9 @@ describe("createExternalState", () => {
|
|
|
123
123
|
it("测试副作用函数", () => {
|
|
124
124
|
const mockSideEffect = vi.fn((...args) => void 0);
|
|
125
125
|
const initialState: string = "initial";
|
|
126
|
-
const state = createExternalState(initialState,
|
|
126
|
+
const state = createExternalState(initialState, {
|
|
127
|
+
sideEffect: mockSideEffect,
|
|
128
|
+
});
|
|
127
129
|
state.set("updated");
|
|
128
130
|
expect(mockSideEffect).toHaveBeenCalledTimes(1);
|
|
129
131
|
expect(mockSideEffect).toHaveBeenCalledWith("updated", initialState);
|
|
@@ -135,7 +137,9 @@ describe("createExternalState", () => {
|
|
|
135
137
|
it("测试异步副作用函数", async () => {
|
|
136
138
|
const mockAsyncSideEffect = vi.fn().mockResolvedValue(undefined);
|
|
137
139
|
const initialState: string = "initial";
|
|
138
|
-
const state = createExternalState(initialState,
|
|
140
|
+
const state = createExternalState(initialState, {
|
|
141
|
+
sideEffect: mockAsyncSideEffect,
|
|
142
|
+
});
|
|
139
143
|
|
|
140
144
|
state.set("updated");
|
|
141
145
|
expect(mockAsyncSideEffect).toHaveBeenCalledTimes(1);
|
|
@@ -181,4 +185,37 @@ describe("createExternalState", () => {
|
|
|
181
185
|
expect(ageLocator.element().textContent).toBe("35");
|
|
182
186
|
expect(state.get()).toEqual({ name: "王五", age: 35 });
|
|
183
187
|
});
|
|
188
|
+
|
|
189
|
+
it("测试transform转换功能", async () => {
|
|
190
|
+
const state = createExternalState("hello", {
|
|
191
|
+
transform: {
|
|
192
|
+
get: (str) => str.toUpperCase(),
|
|
193
|
+
set: (str) => str.toLowerCase(),
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
expect(state.get()).toBe("HELLO");
|
|
198
|
+
|
|
199
|
+
state.set("WORLD");
|
|
200
|
+
expect(state.get()).toBe("WORLD");
|
|
201
|
+
|
|
202
|
+
function TestComponent() {
|
|
203
|
+
const [value, setValue] = state.use();
|
|
204
|
+
return (
|
|
205
|
+
<div>
|
|
206
|
+
<span data-testid="value">{value}</span>
|
|
207
|
+
<button data-testid="update" onClick={() => setValue("TEST")}>
|
|
208
|
+
Update
|
|
209
|
+
</button>
|
|
210
|
+
</div>
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { getByTestId } = render(<TestComponent />);
|
|
215
|
+
expect(getByTestId("value").element().textContent).toBe("WORLD");
|
|
216
|
+
|
|
217
|
+
await getByTestId("update").click();
|
|
218
|
+
expect(getByTestId("value").element().textContent).toBe("TEST");
|
|
219
|
+
expect(state.get()).toBe("TEST");
|
|
220
|
+
});
|
|
184
221
|
});
|
|
@@ -20,41 +20,80 @@ export type ExternalSideEffect<T> = (
|
|
|
20
20
|
prevState: T
|
|
21
21
|
) => any | Promise<any>;
|
|
22
22
|
|
|
23
|
+
/**
|
|
24
|
+
* @en Transform functions for getting and setting state
|
|
25
|
+
* @zh 用于获取和设置状态的转换函数
|
|
26
|
+
* @template T The type of the state / 状态的类型
|
|
27
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
28
|
+
*/
|
|
29
|
+
export interface Transform<T, U = T> {
|
|
30
|
+
/**
|
|
31
|
+
* @en Transform function for getting state
|
|
32
|
+
* @zh 获取状态时的转换函数
|
|
33
|
+
*/
|
|
34
|
+
get?: (state: T) => U;
|
|
35
|
+
/**
|
|
36
|
+
* @en Transform function for setting state
|
|
37
|
+
* @zh 设置状态时的转换函数
|
|
38
|
+
*/
|
|
39
|
+
set?: (value: U) => T;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @en Options for creating external state
|
|
44
|
+
* @zh 创建外部状态的选项
|
|
45
|
+
* @template T The type of the state / 状态的类型
|
|
46
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
47
|
+
*/
|
|
48
|
+
export interface ExternalStateOptions<T, U = T> {
|
|
49
|
+
/**
|
|
50
|
+
* @en Side effect function to run after state changes
|
|
51
|
+
* @zh 状态变更后运行的副作用函数
|
|
52
|
+
*/
|
|
53
|
+
sideEffect?: ExternalSideEffect<T>;
|
|
54
|
+
/**
|
|
55
|
+
* @en Transform functions for getting and setting state
|
|
56
|
+
* @zh 用于获取和设置状态的转换函数
|
|
57
|
+
*/
|
|
58
|
+
transform?: Transform<T, U>;
|
|
59
|
+
}
|
|
60
|
+
|
|
23
61
|
/**
|
|
24
62
|
* @en External state management interface
|
|
25
63
|
* @zh 外部状态管理接口
|
|
26
64
|
* @template T The type of the state / 状态的类型
|
|
65
|
+
* @template U The transformed type for getting / 获取时的转换类型
|
|
27
66
|
*/
|
|
28
|
-
export interface ExternalState<T> {
|
|
67
|
+
export interface ExternalState<T, U = T> {
|
|
29
68
|
/**
|
|
30
69
|
* @en Get the current state value
|
|
31
70
|
* @zh 获取当前状态值
|
|
32
|
-
* @returns The current state value /
|
|
71
|
+
* @returns The current state value (transformed if transform.get is provided) / 当前状态值(如果提供了 transform.get 则进行转换)
|
|
33
72
|
*/
|
|
34
|
-
get: () =>
|
|
73
|
+
get: () => U;
|
|
35
74
|
|
|
36
75
|
/**
|
|
37
76
|
* @en Set a new state value
|
|
38
77
|
* @zh 设置新的状态值
|
|
39
|
-
* @param newState The new state value /
|
|
78
|
+
* @param newState The new state value or a function that returns it / 新的状态值或返回新状态的函数
|
|
40
79
|
*/
|
|
41
|
-
set: (newState:
|
|
80
|
+
set: (newState: U | ((prevState: U) => U)) => void;
|
|
42
81
|
|
|
43
82
|
/**
|
|
44
83
|
* @en React Hook for using external state in components
|
|
45
84
|
* @zh 在组件中使用外部状态的 React Hook
|
|
46
|
-
* @returns Array containing current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
85
|
+
* @returns Array containing current钣金龙8国际唯一官网 current state and update function, similar to useState / 包含当前状态和更新函数的数组,类似于 useState
|
|
47
86
|
*/
|
|
48
|
-
use: () => [
|
|
87
|
+
use: () => [U, (newState: U | ((prevState: U) => U)) => void];
|
|
49
88
|
|
|
50
89
|
/**
|
|
51
90
|
* @zh use的变体,只获取value.
|
|
52
91
|
* @en A variant of use that only gets the value.
|
|
53
92
|
*/
|
|
54
|
-
useGetter: () =>
|
|
93
|
+
useGetter: () => U;
|
|
55
94
|
}
|
|
56
95
|
|
|
57
|
-
export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
96
|
+
export interface ExternalWithKernel<T, U = T> extends ExternalState<T, U> {
|
|
58
97
|
__listeners: CreateStateListener<T>[];
|
|
59
98
|
}
|
|
60
99
|
|
|
@@ -62,12 +101,18 @@ export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
62
101
|
*
|
|
63
102
|
* @example
|
|
64
103
|
* ```tsx
|
|
65
|
-
* // Create an app-level theme state
|
|
66
|
-
* const themeState = createExternalState('light'
|
|
104
|
+
* // Create an app-level theme state with options
|
|
105
|
+
* const themeState = createExternalState('light', {
|
|
106
|
+
* sideEffect: (newState, prevState) => console.log(`Theme changed from ${prevState} to ${newState}`),
|
|
107
|
+
* transform: {
|
|
108
|
+
* get: (state) => state.toUpperCase(),
|
|
109
|
+
* set: (value) => value.toLowerCase()
|
|
110
|
+
* }
|
|
111
|
+
* });
|
|
67
112
|
*
|
|
68
113
|
* // Get or modify state outside components
|
|
69
|
-
* console.log(themeState.get()); // '
|
|
70
|
-
* themeState.set('dark');
|
|
114
|
+
* console.log(themeState.get()); // 'LIGHT'
|
|
115
|
+
* themeState.set((prev) => prev === 'light' ? 'dark' : 'light'); // Toggle theme
|
|
71
116
|
*
|
|
72
117
|
* // Use state in components
|
|
73
118
|
* function ThemeConsumer() {
|
|
@@ -75,7 +120,7 @@ export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
75
120
|
*
|
|
76
121
|
* return (
|
|
77
122
|
* <div className={theme}>
|
|
78
|
-
* <button onClick={() => setTheme(
|
|
123
|
+
* <button onClick={() => setTheme((prev) => prev === 'LIGHT' ? 'DARK' : 'LIGHT')}>
|
|
79
124
|
* Toggle theme / 切换主题
|
|
80
125
|
* </button>
|
|
81
126
|
* </div>
|
|
@@ -83,22 +128,39 @@ export interface ExternalWithKernel<T> extends ExternalState<T> {
|
|
|
83
128
|
* }
|
|
84
129
|
* ```
|
|
85
130
|
*/
|
|
86
|
-
export function createExternalState<T>(
|
|
131
|
+
export function createExternalState<T, U = T>(
|
|
87
132
|
initialState: T | (() => T),
|
|
88
|
-
|
|
89
|
-
): ExternalState<T> {
|
|
133
|
+
options: ExternalStateOptions<T, U> = {}
|
|
134
|
+
): ExternalState<T, U> {
|
|
90
135
|
let state: T =
|
|
91
136
|
typeof initialState === "function"
|
|
92
137
|
? (initialState as () => T)()
|
|
93
138
|
: initialState;
|
|
94
139
|
|
|
95
140
|
const listeners: CreateStateListener<T>[] = [];
|
|
141
|
+
const { sideEffect, transform } = options;
|
|
96
142
|
|
|
97
|
-
const get = () =>
|
|
143
|
+
const get = () => {
|
|
144
|
+
const currentState = state;
|
|
145
|
+
return transform?.get
|
|
146
|
+
? transform.get(currentState)
|
|
147
|
+
: (currentState as unknown as U);
|
|
148
|
+
};
|
|
98
149
|
|
|
99
|
-
const set = (newState:
|
|
150
|
+
const set = (newState: U | ((prevState: U) => U)) => {
|
|
100
151
|
const prevState = state;
|
|
101
|
-
|
|
152
|
+
const transformedPrevState = transform?.get
|
|
153
|
+
? transform.get(prevState)
|
|
154
|
+
: (prevState as unknown as U);
|
|
155
|
+
state = transform?.set
|
|
156
|
+
? transform.set(
|
|
157
|
+
typeof newState === "function"
|
|
158
|
+
? (newState as (prev: U) => U)(transformedPrevState)
|
|
159
|
+
: newState
|
|
160
|
+
)
|
|
161
|
+
: ((typeof newState === "function"
|
|
162
|
+
? (newState as (prev: U) => U)(transformedPrevState)
|
|
163
|
+
: newState) as unknown as T);
|
|
102
164
|
|
|
103
165
|
listeners.forEach((listener) => listener(state));
|
|
104
166
|
if (sideEffect) {
|
|
@@ -112,7 +174,7 @@ export function createExternalState<T>(
|
|
|
112
174
|
};
|
|
113
175
|
|
|
114
176
|
const use = () => {
|
|
115
|
-
const [localState, setLocalState] = React.useState(state);
|
|
177
|
+
const [localState, setLocalState] = React.useState<T>(state);
|
|
116
178
|
|
|
117
179
|
React.useEffect(() => {
|
|
118
180
|
listeners.push(setLocalState);
|
|
@@ -124,7 +186,10 @@ export function createExternalState<T>(
|
|
|
124
186
|
};
|
|
125
187
|
}, []);
|
|
126
188
|
|
|
127
|
-
return [
|
|
189
|
+
return [
|
|
190
|
+
transform?.get ? transform.get(localState) : (localState as unknown as U),
|
|
191
|
+
set,
|
|
192
|
+
] as [U, (newState: U | ((prevState: U) => U)) => void];
|
|
128
193
|
};
|
|
129
194
|
|
|
130
195
|
const useGetter = () => {
|