@@ -39,22 +39,32 @@ function mergeInitialState(
39
39
}
40
40
41
41
/**
42
- * Creates a reducer that opens the channel for external state subscription
43
- * and modification.
42
+ * Creates the base reducer which may be coupled to a specializing reducer.
43
+ * As its final step, for all actions other than CONTROL, the base reducer
44
+ * passes the state and action on through the specializing reducer. The
45
+ * exception for CONTROL actions is because they represent controlled updates
46
+ * from props and no case has yet presented for their specialization.
44
47
*
45
- * This technique uses the "stateReducer" design pattern:
46
- * https://kentcdodds.com/blog/the-state-reducer-pattern/
47
- *
48
- * @param composedStateReducers A custom reducer that can subscribe and modify state.
48
+ * @param composedStateReducers A reducer to specialize state changes.
49
49
* @return The reducer.
50
50
*/
51
51
function inputControlStateReducer (
52
52
composedStateReducers : StateReducer
53
- ) : StateReducer {
53
+ ) : StateReducer < actions . ControlAction > {
54
54
return ( state , action ) => {
55
55
const nextState = { ...state } ;
56
56
57
57
switch ( action . type ) {
58
+ /*
59
+ * Controlled updates
60
+ */
61
+ case actions . CONTROL :
62
+ nextState . value = action . payload . value ;
63
+ nextState . isDirty = false ;
64
+ nextState . _event = undefined ;
65
+ // Returns immediately to avoid invoking additional reducers.
66
+ return nextState ;
67
+
58
68
/**
59
69
* Keyboard events
60
70
*/
@@ -140,7 +150,7 @@ export function useInputControlStateReducer(
140
150
initialState : Partial < InputState > = initialInputControlState ,
141
151
onChangeHandler : InputChangeCallback
142
152
) {
143
- const [ state , dispatch ] = useReducer < StateReducer > (
153
+ const [ state , dispatch ] = useReducer (
144
154
inputControlStateReducer ( stateReducer ) ,
145
155
mergeInitialState ( initialState )
146
156
) ;
@@ -188,10 +198,15 @@ export function useInputControlStateReducer(
188
198
189
199
const currentState = useRef ( state ) ;
190
200
const refProps = useRef ( { value : initialState . value , onChangeHandler } ) ;
201
+
202
+ // Freshens refs to props and state so that subsequent effects have access
203
+ // to their latest values without their changes causing effect runs.
191
204
useLayoutEffect ( ( ) => {
192
205
currentState . current = state ;
193
206
refProps . current = { value : initialState . value , onChangeHandler } ;
194
207
} ) ;
208
+
209
+ // Propagates the latest state through onChange.
195
210
useLayoutEffect ( ( ) => {
196
211
if (
197
212
currentState . current . _event !== undefined &&
@@ -205,14 +220,16 @@ export function useInputControlStateReducer(
205
220
} ) ;
206
221
}
207
222
} , [ state . value , state . isDirty ] ) ;
223
+
224
+ // Updates the state from props.
208
225
useLayoutEffect ( ( ) => {
209
226
if (
210
227
initialState . value !== currentState . current . value &&
211
228
! currentState . current . isDirty
212
229
) {
213
230
dispatch ( {
214
- type : actions . RESET ,
215
- payload : { value : initialState . value } ,
231
+ type : actions . CONTROL ,
232
+ payload : { value : initialState . value ?? '' } ,
216
233
} ) ;
217
234
}
218
235
} , [ initialState . value ] ) ;
0 commit comments