Skip to content
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

[react-redux_v5.x.x] Connect the Dispatch type with actions in dispatch params #3035

Merged
merged 9 commits into from Jan 4, 2019

Conversation

peter-leonov
Copy link
Contributor

Long time no see :)

This PR solves the problem with actions in dispatch props have currently no connection to the Dispatch type argument of the connect() function. In practice this means that Flow will not stop us from returning any random type from an action creator given in mapDispatchToProps. Here is an example:

// first valid action with action creator of our app
type ValidAction1 = {|
  +type: 'VALID_ACTION_1'
|}
const validAction1 = () => ({type 'VALID_ACTION_1'})
// second valid action with action creator of our app
type ValidAction2 = {|
  +type: 'VALID_ACTION_2'
|}
const validAction2 = () => ({type 'VALID_ACTION_1'})

// combined type of all valid actions of our app
type Action = ValidAction1 | ValidAction2

// the dispatch type of our application, which should accept only valid actions of our app
// (vanilla dispatch() returns action as is)
type Dispatch = Action => Action

// here is an example of an invalid action creator (possibly a typo or copy-paste problem)
const invalidAction = () => { foo: 'bar' }

// no own props
type OwnProps = {||}
// this component is going to only dispatch things
type Props = {
  action1: typeof action1,
  action2: typeof action2,
  invalidAction: typeof invalidAction
}

// ... other boilerplate code here ...

// ERROR: currently at this point Flow does not warn us about the `invalidAction`, but is should warn
connect(Props, OwnProps, _, _, _, Dispatch)

We do check the mapStateToProps to get the real state type (if it's present in the code and given or inferable from the call) but fo dispatch the Dispatch type is just bogus and was added to be typed in the bright future. Hopefully, it is now :)

Also, I've fixed some minor things:

  • not passing dispatch to config and mergeProps arguments if no mapStateToProps parameter passed;
  • fixed type arguments order for the mergeProps part of typings (was a typo);
  • pass the empty object type {||} as stateProps to mergeProps and config instead of wrongly inferred empty type for the non present StateProps and DispatchProps.

As this PR makes the typings significantly stricter (and safer) but a bit harder to drop into a random project (especially, after the habits we've developed with the very forgiving mode of Flow <v0.85) we should discuss if and how to make this change smooth 😄

@@ -52,6 +52,8 @@ declare module "react-redux" {
// and provide the StateProps type to the SP type parameter.
| ((state: S, ownProps: OP) => (state: S, ownProps: OP) => SP);

declare type Bind<D> = <A, R>((...A) => R) => (...A) => $Call<D, R>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what bindActionCreators() actually does.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the corresponding change be made to redux libdef as well?

options?: ?Options<S, OP, {||}, {| ...OP, ...DP |}>,
// Got error like inexact OwnProps is incompatible with exact object type?
// Just make your OP parameter an exact object.
): Connector<P, OP, {| ...OP, ...$ObjMap<DP, Bind<D>> |}>;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the main piece of code in this PR:

$ObjMap<DP, Bind<D>>

In simple case it just make sure that the actions returned by the specified in DP action creators are compatible with the function type in D. That it's a function type is implicitly specified by the usage of $ObjMap.

What's more interesting, in case of redux-thunk the code above transforms the raw thunks of type (...A) => (...B) => R from mapDispatchToProps object to (...A) => R in the resulting Props. This thing makes it possible to use thunks returning something useful:

const thunkB = (id) => async (dispatch: Dispatch) => {
  const data = await fetchTodo(id);
  dispatch(todoLoaded(data));
}

const thunkA = async (dispatch: Dispatch) => {
  // dispatch returns the promise from thunkB
  await dispatch(thunkB('todo123'))
}

class TodoLoader ... {
  componentDidMount() {
    const { thunkA } = this.props;
    // so it does in the props too and the new typings support this
    thunkA.then(trackTodoLoaded);
  }
}

// ... and here the property returns a return value of thunk
// as dispatch calls it for us with `dispatch` and `getState`
thunk: () => Promise<number>,
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a test case for redux-thunk as described in previous comment. Here you can see that DispatchProps type as returned from mapDispatchToProps is different to what the Props get. This is exactly because of what connect() does: it hides the arguments by wrapping the thunk value.

Btw, can it be considered as a good monad example, dear functional gurus? 🤓

)(Com);
e.push(Connected);
<Connected passthrough="foo" />;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think, is it time to split the tests for connect in multiple files by topic? Like test_connectMergeProps.js, test_connectStateOnly.js, test_connectConfig.js.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that could make sense! Good idea 👍

@EugeneZ
Copy link
Contributor

EugeneZ commented Dec 31, 2018

This looks great. Massive thanks for all the work you've been doing on these typings! I think there is one overload of connect missing, the one where mapStateToProps is provided, and the mapDispatchToProps is a map. (The corresponding overload where the third and fourth arguments are provided is present, however.)

In our code, at least, this seems to be the most commonly used overload.

I've taken a shot at typing these in our code based on your work and they seem to work well so far:

  // this difference is not important in the vanila redux,
  // but in case of usage with redux-thunk, the return type may differ.
  declare export function connect<-P, -OP, -SP, -DP, S, D>(
    // If you get error here try adding return type to your mapStateToProps function
    mapStateToProps: MapStateToProps<S, OP, SP>,
    mapDispatchToProps: DP,
    mergeProps?: null | void,
    options?: ?Options<S, OP, SP, {| ...OP, ...SP, ...DP |}>,
    // Got error like inexact OwnProps is incompatible with exact object type?
    // Just make your OP parameter an exact object.
  ): Connector<P, OP, {| ...OP, ...SP, ...$ObjMap<DP, Bind<D>> |}>;

@peter-leonov
Copy link
Contributor Author

@EugeneZ 🤦‍♀️ yes, you're right. It was not exactly forgotten, but embedded into the function case and thus typed incorrectly in case of thunks. Thanks a lot for trying out the yet very raw material! And happy new year 🎄🎅🍋!

Copy link
Member

@villesau villesau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea to split to files, and one (apparently?) typo :)

@@ -548,4 +548,140 @@ function testHoistConnectedComponent() {
<Connected passthrough={123} passthroughWithDefaultProp={456} forMapStateToProps={'data'}/>;
// OK with declared static property
Connected.myStatic;
//$ExpectError property `myStatic1` is missing in statics
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean notStatic? :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Eagle eye achievement unlocked!

)(Com);
e.push(Connected);
<Connected passthrough="foo" />;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that could make sense! Good idea 👍

@peter-leonov
Copy link
Contributor Author

Rebased to hopefully get green tests. Will split into files asap.

@villesau villesau merged commit 5848599 into flow-typed:master Jan 4, 2019
@villesau
Copy link
Member

villesau commented Jan 4, 2019

Nice, GJ!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants