1
1
import { makeStyles } from "@fluentui/react-components" ;
2
- import { isFunction , isNotEmptyArray , isNullOrEmptyString , isPrimitiveValue , jsonClone , jsonStringify , LoggerLevel , objectsEqual , wrapFunction } from "@kwiz/common" ;
2
+ import { isFunction , isNotEmptyArray , isNullOrEmptyString , isPrimitiveValue , jsonClone , jsonStringify , LoggerLevel , objectsEqual } from "@kwiz/common" ;
3
3
import { HTMLAttributes , MutableRefObject , SetStateAction , useCallback , useEffect , useRef , useState } from "react" ;
4
4
import { GetLogger } from "../_modules/config" ;
5
5
import { mixins } from "../styles/styles" ;
@@ -8,7 +8,7 @@ import { mixins } from "../styles/styles";
8
8
export const useEffectOnlyOnMount = [ ] ;
9
9
10
10
type stateExOptions < ValueType > = {
11
- onChange ?: ( newValue : SetStateAction < ValueType > , isValueChanged : boolean ) => SetStateAction < ValueType > ;
11
+ onChange ?: ( newValue : ValueType , isValueChanged : boolean ) => ValueType ;
12
12
//will not set state if value did not change
13
13
skipUpdateIfSame ?: boolean ;
14
14
//optional, provide a name for better logging
@@ -38,8 +38,18 @@ export function useStateEX<ValueType>(initialValue: ValueType, options?: stateEx
38
38
let logger = GetLogger ( `useStateWithTrack${ isNullOrEmptyString ( name ) ? '' : ` ${ name } ` } ` ) ;
39
39
logger . setLevel ( LoggerLevel . WARN ) ;
40
40
41
- const [ value , setValueInState ] = useState ( initialValue ) ;
42
- const currentValue = useRef ( value ) ;
41
+ const [ value , setValueInState ] = useState < ValueType > ( initialValue ) ;
42
+
43
+ const currentValue = useRef < ValueType > ( ) ;
44
+ //json clone complex/ref values so we can compare if value changed, in case caller makes chagnes on the value object directly.
45
+ const currentValueForChecks = useRef < ValueType > ( ) ;
46
+ useEffect ( ( ) => {
47
+ updateCurrentRef ( initialValue ) ;
48
+ } , useEffectOnlyOnMount ) ;
49
+
50
+ //keep a ref to onChange so the caller's latet state is accessible
51
+ const onChange = useRef ( options . onChange ) ;
52
+ onChange . current = options . onChange ;
43
53
44
54
/** make this a collection in case several callers are awaiting the same propr update */
45
55
const resolveState = useRef < ( ( v : ValueType ) => void ) [ ] > ( [ ] ) ;
@@ -62,53 +72,42 @@ export function useStateEX<ValueType>(initialValue: ValueType, options?: stateEx
62
72
} ;
63
73
useEffect ( ( ) => {
64
74
resolvePromises ( ) ;
65
- if ( isNotEmptyArray ( resolveState . current ) ) {
66
- logger . log ( `resolved after render` ) ;
67
- let resolvers = resolveState . current . slice ( ) ;
68
- resolveState . current = [ ] ; //clear
69
- resolvers . map ( r => r ( value ) ) ;
70
- }
71
- } , [ value , resolveState . current ] ) ;
75
+ } , [ value ] ) ;
72
76
73
77
function getIsValueChanged ( newValue : ValueType ) : boolean {
78
+ let error : Error = null ;
74
79
let result : boolean ;
75
- if ( ! objectsEqual ( newValue as object , currentValue . current as object ) ) {
80
+ try {
81
+ if ( ! objectsEqual ( newValue as object , currentValueForChecks . current as object ) ) {
82
+ result = true ;
83
+ }
84
+ else {
85
+ result = false ;
86
+ }
87
+ } catch ( e ) {
88
+ error = e ;
76
89
result = true ;
77
90
}
78
- else {
79
- result = false ;
80
- }
81
91
82
92
return logger . groupSync ( result ? 'value changed' : 'value not changed' , log => {
83
93
if ( logger . getLevel ( ) === LoggerLevel . VERBOSE ) {
84
- log ( 'old: ' + extractStringValue ( currentValue . current ) ) ;
94
+ log ( 'old: ' + extractStringValue ( currentValueForChecks . current ) ) ;
85
95
log ( 'new: ' + extractStringValue ( newValue ) ) ;
96
+ if ( error ) log ( { label : "Error" , value : error } ) ;
86
97
}
87
98
return result ;
88
99
} ) ;
89
100
} ;
90
-
91
- let setValueWithCheck = ! options . skipUpdateIfSame ? setValueInState : ( newValue : ValueType ) => {
92
- const isValueChanged = getIsValueChanged ( newValue ) ;
93
- if ( isValueChanged ) {
94
- setValueInState ( newValue ) ;
95
- }
96
- else {
97
- resolvePromises ( ) ;
98
- }
99
- }
100
-
101
-
102
- let setValueWithEvents = wrapFunction ( setValueWithCheck , {
103
- before : ( newValue : ValueType ) => isFunction ( options . onChange ) ? options . onChange ( newValue , getIsValueChanged ( newValue ) ) : newValue ,
104
- after : ( newValue : ValueType ) => currentValue . current = isPrimitiveValue ( newValue ) || isFunction ( newValue )
101
+ function updateCurrentRef ( newValue : ValueType ) {
102
+ currentValue . current = newValue ;
103
+ currentValueForChecks . current = isPrimitiveValue ( newValue ) || isFunction ( newValue )
105
104
? newValue
106
105
//fix skipUpdateIfSame for complex objects
107
106
//if we don't clone it, currentValue.current will be a ref to the value in the owner
108
107
//and will be treated as unchanged object, and it will be out of sync
109
108
//this leads to skipUpdateIfSame failing after just 1 unchanged update
110
- : jsonClone ( newValue ) as ValueType
111
- } ) ;
109
+ : jsonClone ( newValue ) as ValueType ;
110
+ }
112
111
113
112
const setValue = useCallback ( ( newState : ValueType ) => new Promise < ValueType > ( resolve => {
114
113
if ( ! isMounted . current ) {
@@ -118,9 +117,22 @@ export function useStateEX<ValueType>(initialValue: ValueType, options?: stateEx
118
117
}
119
118
else {
120
119
resolveState . current . push ( resolve ) ;
121
- setValueWithEvents ( newState ) ;
120
+ const isChanged = isFunction ( onChange . current ) || options . skipUpdateIfSame
121
+ ? getIsValueChanged ( newState ) //don't call this if there is no onChange handler and if we don't need to monitor skipUpdateIfSame
122
+ : true ;
123
+ if ( isFunction ( onChange . current ) )
124
+ newState = onChange . current ( newState , isChanged ) ;
125
+
126
+ //keep current value ref up to date
127
+ updateCurrentRef ( newState ) ;
128
+
129
+ //set state
130
+ if ( ! options . skipUpdateIfSame || isChanged )
131
+ setValueInState ( newState ) ;
132
+ else //don't set in state - just resolve pending promises, UI will not be updated.
133
+ resolvePromises ( ) ;
122
134
}
123
- } ) , [ ] ) ;
135
+ } ) , useEffectOnlyOnMount ) ;
124
136
125
137
return [ value , setValue , currentValue ] ;
126
138
}
0 commit comments