Skip to content

Commit 231498b

Browse files
committed
fixed useStateEX to keep onchange with latest state
1 parent 39d68fd commit 231498b

File tree

3 files changed

+52
-40
lines changed

3 files changed

+52
-40
lines changed

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
"dependencies": {
6262
"@fluentui/react-datepicker-compat": "^0.4.53",
6363
"@fluentui/react-timepicker-compat": "^0.2.42",
64-
"@kwiz/common": "^1.0.120",
64+
"@kwiz/common": "^1.0.128",
6565
"@mismerge/core": "^1.2.1",
6666
"@mismerge/react": "^1.0.1",
6767
"esbuild": "^0.19.12",

src/helpers/hooks.tsx

Lines changed: 47 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
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";
33
import { HTMLAttributes, MutableRefObject, SetStateAction, useCallback, useEffect, useRef, useState } from "react";
44
import { GetLogger } from "../_modules/config";
55
import { mixins } from "../styles/styles";
@@ -8,7 +8,7 @@ import { mixins } from "../styles/styles";
88
export const useEffectOnlyOnMount = [];
99

1010
type stateExOptions<ValueType> = {
11-
onChange?: (newValue: SetStateAction<ValueType>, isValueChanged: boolean) => SetStateAction<ValueType>;
11+
onChange?: (newValue: ValueType, isValueChanged: boolean) => ValueType;
1212
//will not set state if value did not change
1313
skipUpdateIfSame?: boolean;
1414
//optional, provide a name for better logging
@@ -38,8 +38,18 @@ export function useStateEX<ValueType>(initialValue: ValueType, options?: stateEx
3838
let logger = GetLogger(`useStateWithTrack${isNullOrEmptyString(name) ? '' : ` ${name}`}`);
3939
logger.setLevel(LoggerLevel.WARN);
4040

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;
4353

4454
/** make this a collection in case several callers are awaiting the same propr update */
4555
const resolveState = useRef<((v: ValueType) => void)[]>([]);
@@ -62,53 +72,42 @@ export function useStateEX<ValueType>(initialValue: ValueType, options?: stateEx
6272
};
6373
useEffect(() => {
6474
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]);
7276

7377
function getIsValueChanged(newValue: ValueType): boolean {
78+
let error: Error = null;
7479
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;
7689
result = true;
7790
}
78-
else {
79-
result = false;
80-
}
8191

8292
return logger.groupSync(result ? 'value changed' : 'value not changed', log => {
8393
if (logger.getLevel() === LoggerLevel.VERBOSE) {
84-
log('old: ' + extractStringValue(currentValue.current));
94+
log('old: ' + extractStringValue(currentValueForChecks.current));
8595
log('new: ' + extractStringValue(newValue));
96+
if (error) log({ label: "Error", value: error });
8697
}
8798
return result;
8899
});
89100
};
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)
105104
? newValue
106105
//fix skipUpdateIfSame for complex objects
107106
//if we don't clone it, currentValue.current will be a ref to the value in the owner
108107
//and will be treated as unchanged object, and it will be out of sync
109108
//this leads to skipUpdateIfSame failing after just 1 unchanged update
110-
: jsonClone(newValue) as ValueType
111-
});
109+
: jsonClone(newValue) as ValueType;
110+
}
112111

113112
const setValue = useCallback((newState: ValueType) => new Promise<ValueType>(resolve => {
114113
if (!isMounted.current) {
@@ -118,9 +117,22 @@ export function useStateEX<ValueType>(initialValue: ValueType, options?: stateEx
118117
}
119118
else {
120119
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();
122134
}
123-
}), []);
135+
}), useEffectOnlyOnMount);
124136

125137
return [value, setValue, currentValue];
126138
}

0 commit comments

Comments
 (0)