-
Notifications
You must be signed in to change notification settings - Fork 4.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
InputControl
: Fix acceptance of falsy values in controlled updates
#42484
InputControl
: Fix acceptance of falsy values in controlled updates
#42484
Conversation
Size Change: +48 B (0%) Total Size: 1.26 MB
ℹ️ View Unchanged
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the quick turnaround and fix @stokesman 🚀
✅ Updated unit test fails as expected on initial commit on the branch
✅ Could still replicate the issue in both Storybook & editors
✅ After checking out latest on PR branch the unit tests pass
✅ Fix resolves the original issue in the editor & storybook.
✅ Components are still functioning in Storybook
Before | After |
---|---|
Screen.Recording.2022-07-18.at.10.26.09.am.mp4 |
Screen.Recording.2022-07-18.at.10.45.56.am.mp4 |
LGTM!
Would it make sense to add a unit test to avoid future regressions? |
That was why I tweaked the test in eb9e1bc. I think it suffices but let me know if you see a fault with that. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the reply — I had missed that small change in my first pass of this PR.
With regards to the changes in this PR, it would be good if we could find an approach that preserves the reducer's "purity" (i.e. accepting action objects).
Since you mentioned this snippet related to the dispatch RESET
action
gutenberg/packages/components/src/input-control/reducer/reducer.ts
Lines 98 to 101 in 875a204
case actions.RESET: | |
nextState.error = null; | |
nextState.isDirty = false; | |
nextState.value = action.payload.value || state.initialValue; |
Wouldn't be possible to use the null coalescing operator ??
to let falsy values through?
nextState.value = action.payload.value ?? state.initialValue;
That would work for empty strings or
We can add another action (we used to have |
That would my preferred solution — if the
The advantage that I see is that we would keep |
Same. If there are no significant tradeoffs, I would prefer an explicit action type. At least as a casual code reader, I would expect there to always be a |
5dd5fd6
to
633709a
Compare
InputControl
-based componentsInputControl
: Fix acceptance of falsy values in controlled updates
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes test well — I just left a few more comments, but this is looking close!
@@ -19,7 +19,7 @@ export interface InputState { | |||
value?: string; | |||
} | |||
|
|||
export type StateReducer = Reducer< InputState, InputAction >; | |||
export type StateReducer = Reducer< InputState, InputAction | ControlAction >; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the ControlAction
be part of the InputAction
type ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see why you'd ask. Looking at this more closely, given we are okay with the way I've wired up CONTROL
to be exclusively handled by the base reducer, then I'll say no, and yet it's still not quite right as is. The extending reducers will never receive CONTROL
actions so StateReducer
would be too loose a type for those.
I'm thinking we should we keep ControlAction
out of both InputAction
and StateReducer
then make only the base reducer typed to accept it.
diff --git a/packages/components/src/input-control/reducer/reducer.ts b/packages/components/src/input-control/reducer/reducer.ts
index 3f3302608d..a5f80026a6 100644
--- a/packages/components/src/input-control/reducer/reducer.ts
+++ b/packages/components/src/input-control/reducer/reducer.ts
@@ -1,7 +1,7 @@
/**
* External dependencies
*/
-import type { SyntheticEvent, ChangeEvent, PointerEvent } from 'react';
+import type { SyntheticEvent, ChangeEvent, PointerEvent, Reducer } from 'react';
/**
* WordPress dependencies
@@ -50,7 +50,7 @@ function mergeInitialState(
*/
function inputControlStateReducer(
composedStateReducers: StateReducer
-): StateReducer {
+): Reducer< InputState, actions.InputAction | actions.ControlAction > {
return ( state, action ) => {
const nextState = { ...state };
@@ -151,7 +151,7 @@ export function useInputControlStateReducer(
initialState: Partial< InputState > = initialInputControlState,
onChangeHandler: InputChangeCallback
) {
- const [ state, dispatch ] = useReducer< StateReducer >(
+ const [ state, dispatch ] = useReducer(
inputControlStateReducer( stateReducer ),
mergeInitialState( initialState )
);
diff --git a/packages/components/src/input-control/reducer/state.ts b/packages/components/src/input-control/reducer/state.ts
index 53a49aa765..2ca9edaa27 100644
--- a/packages/components/src/input-control/reducer/state.ts
+++ b/packages/components/src/input-control/reducer/state.ts
@@ -6,7 +6,7 @@ import type { Reducer, SyntheticEvent } from 'react';
/**
* Internal dependencies
*/
-import type { ControlAction, InputAction } from './actions';
+import type { InputAction } from './actions';
export interface InputState {
_event?: SyntheticEvent;
@@ -19,7 +19,7 @@ export interface InputState {
value?: string;
}
-export type StateReducer = Reducer< InputState, InputAction | ControlAction >;
+export type StateReducer = Reducer< InputState, InputAction >;
export const initialStateReducer: StateReducer = ( state: InputState ) => state;
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach that you propose sounds reasonable — maybe we could experiment with generics in the StateReducer
type, so that we keep all type defs in the same file?
Something like this
diff --git a/packages/components/src/input-control/reducer/reducer.ts b/packages/components/src/input-control/reducer/reducer.ts
index 3f3302608d..c41ae85775 100644
--- a/packages/components/src/input-control/reducer/reducer.ts
+++ b/packages/components/src/input-control/reducer/reducer.ts
@@ -50,7 +50,7 @@ function mergeInitialState(
*/
function inputControlStateReducer(
composedStateReducers: StateReducer
-): StateReducer {
+): StateReducer< actions.ControlAction > {
return ( state, action ) => {
const nextState = { ...state };
@@ -151,7 +151,7 @@ export function useInputControlStateReducer(
initialState: Partial< InputState > = initialInputControlState,
onChangeHandler: InputChangeCallback
) {
- const [ state, dispatch ] = useReducer< StateReducer >(
+ const [ state, dispatch ] = useReducer(
inputControlStateReducer( stateReducer ),
mergeInitialState( initialState )
);
diff --git a/packages/components/src/input-control/reducer/state.ts b/packages/components/src/input-control/reducer/state.ts
index 53a49aa765..839ac51383 100644
--- a/packages/components/src/input-control/reducer/state.ts
+++ b/packages/components/src/input-control/reducer/state.ts
@@ -6,7 +6,7 @@ import type { Reducer, SyntheticEvent } from 'react';
/**
* Internal dependencies
*/
-import type { ControlAction, InputAction } from './actions';
+import type { InputAction } from './actions';
export interface InputState {
_event?: SyntheticEvent;
@@ -19,7 +19,10 @@ export interface InputState {
value?: string;
}
-export type StateReducer = Reducer< InputState, InputAction | ControlAction >;
+export type StateReducer< TAdditionalAction = {} > = Reducer<
+ InputState,
+ InputAction | TAdditionalAction
+>;
export const initialStateReducer: StateReducer = ( state: InputState ) => state;
We should probably also add a comment about inputControlStateReducer
being the only reducer exposing / handling the CONTROL
action — which means that it's an action that doesn't get exposed to external reducer middleware, meant to be used only internally. Maybe in the inputControlStateReducer
's JSDocs ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 9aef4eb though it required a touch of the Action
type as well. At first I didn't favor the idea generalizing the StateReducer
type for a singular exception to its shape but given that we're interested in refactoring to a hook+component combo #41264 (comment) there will be opportunity make more use of the generic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
…add a comment about inputControlStateReducer being the only reducer exposing / handling the CONTROL action
Done in f394357
Feedback is hopefully addressed satisfactorily. In case not, I've left the rebase/conflict pending for the moment. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Feedback is hopefully addressed satisfactorily. In case not, I've left the rebase/conflict pending for the moment.
I left one last comment, but the changes required should not add runtime changes to this PR.
When that's addressed, we can rebase and give this PR a final check before merging :)
export type StateReducer< SpecializedAction = Action > = Reducer< | ||
InputState, | ||
InputAction | SpecializedAction | ||
>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the current state of this PR, the StateReducer
type would accept any action-like object (this can be verified by inspecting action.type
and seeing what possible values are attributed to it by the TypeScript engine).
Instead, what we'd like to achieve is:
- When
SpecializedAction
is not defined,StateReducer
should only allow actions from theInputActions
type - When
SpecializedAction
is defined,StateReducer
should only allow actions from either theInputActions
type, or theSpecializedAction
type (although we need to make sure thatSpecializedAction
is a validAction
).
I played around with code a bit and I think this should be a good solution:
export type StateReducer< SpecializedAction = Action > = Reducer< | |
InputState, | |
InputAction | SpecializedAction | |
>; | |
export type StateReducer< SpecializedAction = {} > = Reducer< | |
InputState, | |
SpecializedAction extends Action | |
? InputAction | SpecializedAction | |
: InputAction | |
>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very neat Marco! Applied in 30d7c31. I don't recall having seen such logic in typing. TIL 🙌
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you! I learnt this technique by reading the definition of the WordPressComponentProps
type
gutenberg/packages/components/src/ui/context/wordpress-component.ts
Lines 19 to 24 in 33d84b0
( IsPolymorphic extends true | |
? { | |
/** The HTML element or React component to render the component as. */ | |
as?: T | keyof JSX.IntrinsicElements; | |
} | |
: {} ); |
Co-Authored-By: Marco Ciampini <1083581+ciampo@users.noreply.github.com>
3b54cff
to
30d7c31
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM 🚀
Thank you as always for your work, @stokesman !
Thanks for testing and reviewing Aaron and Marco 🙇 |
What?
Makes
InputControl
handling of updates from props distinct from any other action.Why?
To fix #42455. Presently the
RESET
action is used to handle updates from props and that favors a fallback value over a falsy one:gutenberg/packages/components/src/input-control/reducer/reducer.ts
Lines 98 to 101 in 875a204
That means that that falsy values coming from props will not be used which is the cause of the issue.
How?
Testing Instructions
npm test-unit input-control
to verify the changed test failsgit switch -