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

Rewrite MiddlewareArray and gDM for better Dispatch inference #2001

Merged
merged 4 commits into from
Feb 6, 2022

Conversation

markerikson
Copy link
Collaborator

@markerikson markerikson commented Feb 5, 2022

This is an attempt to rewrite the type inference for the middleware arg in configureStore, to better capture cases where a custom middleware overrides the return type of dispatch.

The motivation for this is specifically an issue with the action listener middleware:

const unsubscribe = store.dispatch(addListenerAction({actionCreator, listener});

// ERROR: _should_ be a void callback, but is typed as `{type: 'alm/add', payload: listenerEntry}`
unsubscribe()

Initial investigation shows that part of the issue is with the middleware itself, due to adding fields like addListener to the returned function. Additionally, use of TypedAddListenerAction seems to help.

However, separate to that, I can reproduce the problem with a dummy middleware:

const dummyMiddleware: Middleware<
        {
          (action: Action<'actionListenerMiddleware/add'>): Unsubscribe
        },
        CounterState
      > = (storeApi) => (next) => (action) => {}

      const store1 = configureStore({
        reducer: counterSlice.reducer,
        // PROBLEM:
        middleware: (gDM) => gDM().prepend(dummyMiddleware),

        // WORKS
        //middleware: [dummyMiddleware, thunk]
      })

      const unsubscribe = store1.dispatch(
        addListenerAction({
          actionCreator: testAction1,
          listener: () => {},
        })
      )

Given that, it seems that something is wonky with the way configureStore infers the extensions to dispatch from the middleware types in conjunction with getDefaultMiddleware.

While the commit is a mess, the overall approach here is:

  • Rewrite the MiddlewareArray type so that it tracks a specific tuple of middleware types, rather than "an array that could be any one of these"
  • Use some of the same tuple iteration and intersection techniques we used on Reselect to extract the actual dispatch extension types and intersect them
  • Get a more exact final type for dispatch using that intersected type

What I have here mostly works, except for the "null extraArg in ThunkAction" case. This is because the ThunkMiddlewareFor type previously returned a union of two ThunkMiddleware types with different values for ExtraArgument, one undefined and one null. This was okay when the middleware types were all getting unioned, but now that I've converted them to be a tuple, that doesn't work.

I'd prefer to drop the null case if possible. The comment says "this is back-compat, since it was shown in the docs", but I don't know where/when the docs showed that. Unfortunately this seems to be something that is actually used in the wild:

https://sourcegraph.com/search?q=context:global+lang:typescript+ThunkAction%3C.%2B%2C.%2B%2C.*null.*%3E&patternType=regexp

Granted, several of the hits I looked at are still using the core createStore instead of RTK, but that's still some potential breakage I'd like to avoid if possible.

Update

Lenz and I have worked through the original implementation issues. Current status:

  • We've gotten seemingly correct inference working for middleware, and I've cleaned up the code
  • Given that the extraArg: null pattern is not something we want people to use (unknown would be correct today), and that there's only a couple of examples using it with RTK in the wild, we're comfortable treating that as a bug that needs to be fixed rather than a breaking change and just dropping support for that case
  • The MiddlewareArray.typetests.ts file began failing in this PR. It appears to have been somehow related to the way we do a "package + install" step in our CI builds, while that file also tried to do an import of some local types. The workaround was to rewrite it to actually call configureStore for real instead of faking it at the type level.
  • The types changes here now fail in TS 4.0 and earlier. Given that Reselect now requires TS 4.1 anyway, and 4.6 is right around the corner, it's time to drop 3.9 and 4.0 from our support matrix
  • We plan to release the middleware in RTK 1.8, but there's a chance we might need to do a 1.7.x patch release before then. So, I've created a new v1.8.0-integration branch, and will merge these changes into there
  • The middleware itself will need additional API changes, per #1648 (comment) , and I'll do those as a separate branch + PR

@codesandbox-ci
Copy link

codesandbox-ci bot commented Feb 5, 2022

This pull request is automatically built and testable in CodeSandbox.

To see build info of the built libraries, click here or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 3ff7137:

Sandbox Source
Vanilla Configuration
Vanilla Typescript Configuration
rsk-github-issues-example Configuration
@examples-query-react/basic Configuration
@examples-query-react/advanced Configuration
@examples-action-listener/counter Configuration

@github-actions
Copy link

github-actions bot commented Feb 5, 2022

size-limit report 📦

Path Size
1. entry point: @reduxjs/toolkit (cjs.production.min.js) 12.93 KB (0%)
1. entry point: @reduxjs/toolkit (esm.js) 10.79 KB (0%)
1. entry point: @reduxjs/toolkit/query (cjs.production.min.js) 22.67 KB (0%)
1. entry point: @reduxjs/toolkit/query (esm.js) 19.04 KB (0%)
1. entry point: @reduxjs/toolkit/query/react (cjs.production.min.js) 24.91 KB (0%)
1. entry point: @reduxjs/toolkit/query/react (esm.js) 21.73 KB (0%)
2. entry point: @reduxjs/toolkit (without dependencies) (cjs.production.min.js) 5.71 KB (0%)
2. entry point: @reduxjs/toolkit (without dependencies) (esm.js) 5.67 KB (0%)
2. entry point: @reduxjs/toolkit/query (without dependencies) (cjs.production.min.js) 10.12 KB (0%)
2. entry point: @reduxjs/toolkit/query (without dependencies) (esm.js) 10.55 KB (0%)
2. entry point: @reduxjs/toolkit/query/react (without dependencies) (cjs.production.min.js) 2.8 KB (0%)
2. entry point: @reduxjs/toolkit/query/react (without dependencies) (esm.js) 3.21 KB (0%)
3. createSlice (esm.js) 5.03 KB (0%)
3. createEntityAdapter (esm.js) 6.28 KB (0%)
3. configureStore (esm.js) 5.65 KB (0%)
3. createApi (esm.js) 17.24 KB (0%)
3. createApi (react) (esm.js) 19.95 KB (0%)
3. fetchBaseQuery (esm.js) 11.63 KB (0%)
3. setupListeners (esm.js) 10.42 KB (0%)
3. ApiProvider (esm.js) 18.58 KB (0%)

@netlify
Copy link

netlify bot commented Feb 5, 2022

✔️ Deploy Preview for redux-starter-kit-docs ready!

🔨 Explore the source changes: 5ad7019

🔍 Inspect the deploy log: https://app.netlify.com/sites/redux-starter-kit-docs/deploys/61ff52f9ad3e41000799fbb3

😎 Browse the preview: https://deploy-preview-2001--redux-starter-kit-docs.netlify.app

@markerikson markerikson marked this pull request as ready for review February 5, 2022 22:03
@markerikson
Copy link
Collaborator Author

markerikson commented Feb 5, 2022

Weird - I force-pushed the branch, and I can see the new commit in https://github.com/reduxjs/redux-toolkit/commits/feature/gdm-array-types , but the PR isn't updating properly.

edit

Seems like Github Actions may have been having some issues earlier

@markerikson markerikson force-pushed the feature/gdm-array-types branch 4 times, most recently from e21d329 to 5ad7019 Compare February 6, 2022 04:47
@markerikson markerikson changed the title WIP Rewrite MiddlewareArray and gDM for better inference Rewrite MiddlewareArray and gDM for better Dispatch inference Feb 6, 2022
@markerikson markerikson changed the base branch from master to v1.8.0-integration February 6, 2022 17:12
@markerikson markerikson force-pushed the feature/gdm-array-types branch from 46eb2b3 to 3b1e697 Compare February 6, 2022 17:23
`getDefaultMiddleware` defines an initial type that may or may not
include `thunk`, depending on options. From there, `EnhancedStore`
tries to extract any extensions to `dispatch` from "the type of all
middleware as an array", to fully infer the correct type of
`store.dispatch`.

This was flaky. In particular, the `MiddlewareArray` type ended up
with "a union of all middleware" type, and that meant the extensions
weren't always getting read correctly.

This commit rewrites the inference to be more correct:

- `MiddlewareArray` now tracks an exact tuple type for its contents
- `.concat/prepend` now update that middleware tuple type
- We exactly extract the dispatch extensions from the tuple,
  and retain the exact order of the middleware as declared
- All the extensions are intersected together with `Dispatch`

This should now correctly represent cases like the listener
middleware returning an `Unsubscribe` callback in response to
`dispatch(addListenerAction())`

This commit also drops support for passing `null` as the `extraArg`
generic for `ThunkAction`, as that is an outdated pattern and
we want to discourage it.
@monim67
Copy link

monim67 commented Oct 23, 2023

I can't seem to find any issue related to this, is this bug fixed? If yes from which version? I am also facing the unsubscribe type as action bug, is there any work-around?

@phryneas
Copy link
Member

@monim67 This is a Pull Request that was merged into 1.8.0 one and a half years ago.

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.

3 participants