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

Consider removing DefaultRootState type #1879

Closed
markerikson opened this issue Mar 20, 2022 · 11 comments
Closed

Consider removing DefaultRootState type #1879

markerikson opened this issue Mar 20, 2022 · 11 comments
Labels
Milestone

Comments

@markerikson
Copy link
Contributor

React-Redux v8 currently has a DefaultRootState type defined:

react-redux/src/types.ts

Lines 13 to 23 in 4d38402

/**
* This interface can be augmented by users to add default types for the root state when
* using `react-redux`.
* Use module augmentation to append your own type definition in a your_custom_type.d.ts file.
* https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
*/
// tslint:disable-next-line:no-empty-interface
export interface DefaultRootState {}
export type AnyIfEmpty<T extends object> = keyof T extends never ? any : T
export type RootStateOrAny = AnyIfEmpty<DefaultRootState>

This was copy-pasted directly from the @types/react-redux types in DefinitelyTyped, which have had that for a while.

We Redux maintainers were not involved in that type being added originally, and specifically think it's a bad practice:

Or to say it bluntly: If at that point in time, we as Redux maintainers would have known about what was going on in DefinitelyTyped, we would have prevented DefaultRootState from ever being added. Unfortunately, at that point we were not aware.

It is a global enhancement of the state, no matter if the reducer was actually added to your store.

At one point in time, some library will go ahead and pollute that interface and suddenly every project using that library will have an extra property in their Redux store - without any reducer for it being present.

There is a good reason we don't recommend doing this anywhere in the docs.

Given that, I'd like to propose removing it from the v8 typings before this gets released.

I'm assuming we'd want to replace that with unknown instead.

I did a couple code searches, and I see maybe 1000-1500 usages in the wild:

So that's not zero, but it isn't a thing that would completely break the ecosystem in the upgrade process.

Lenz also said that this is a pattern that could get replicated in userspace if desired, although I'm not sure what the details are on that.

@markerikson markerikson added the v8 label Mar 20, 2022
@markerikson markerikson added this to the 8.0 milestone Mar 20, 2022
@phryneas
Copy link
Member

phryneas commented Mar 20, 2022

The benefits of the pattern are not to be overlooked - you don't need to have all reducers meet in one place, which would be especially desirable in a situation where reducers are injected after the fact.

It could, however, easily be recreated in userland:

// store/state.ts
export interface MyOwnRootState {}

// store/hooks.ts
import { MyOwnRootState } from './state'
import { TypedUseSelectorHook, useSelector  } from 'react-redux'
export const useAppSelector: TypedUseSelectorHook<MyOwnRootState > = useSelector

// feature/something/slice.ts
declare module 'store/state' {
  inferface MyOwnRootState {
    foo: MyFeatureState
  }
}

=> this does not necessarily need to be shipped with the react-redux types and can be imitated in userland. That way, it's also impossible for random dependencies to poison that type.

And it also clearly makes it an "edge case useful in some scenarios" instead of a "first-grade supported" pattern, which I would very much prefer.

@phryneas
Copy link
Member

One point where my previous statement would not hold true:
Afaik, there is no way to create a connect type that has a fixed State type at the moment.

But in that light I would suggest we add another type

export type TypedConnect<State, Dispatch> = { /* all the current signatures with fixed state and dispatch types */ }
export const connect: TypedConnect<unknown, Dispatch>

that TypedConnect type will probably end up being more useful than the DefaultRootState in the first place.

@josh-degraw
Copy link

Makes total sense, even if for no other reason than consistency with how the other redux codebases do things.

@dai-shi
Copy link
Contributor

dai-shi commented Mar 21, 2022

Module Augmentation should be useful in case we want a TS-only solution for typing state. This sounds reasonable because 99% usage of redux would be single store. I kind of liked the solution, but can understand that it may lead to a bad practice.

Just my two cents.

@markerikson
Copy link
Contributor Author

markerikson commented Apr 10, 2022

Just did this in #1887 . There was also already a Connect<State = DefaultRootState> type being exported, so that just defaults to unknown now.

(That Connect type could theoretically be modified to accept a Dispatch generic too, but it looks like it would be complicated to fit in and frankly I don't want to worry about it.)

@adamaveray
Copy link

In case anyone is looking to replicate augmenting the DefaultRootState type (to avoid having to explicitly declare the state's type in every call to useSelector()) the following module augmentation file should work:

// react-redux.d.ts
import type { EqualityFn } from 'react-redux/es/types';
import type { GlobalState } from '...'; // Your root store type

declare module 'react-redux' {
  export declare const useSelector: <T>(selector: (state: GlobalState) => T, equalityFn?: EqualityFn<T>) => T;
}

(The file should be placed in a directory listed in the project's tsconfig.json's typeRoots array – see the docs)

Now calls such as useSelector(state => ...) will automatically treat the state parameter as having the above GlobalState type.

@longb1997
Copy link

In case anyone is looking to replicate augmenting the DefaultRootState type (to avoid having to explicitly declare the state's type in every call to useSelector()) the following module augmentation file should work:

// react-redux.d.ts
import type { EqualityFn } from 'react-redux/es/types';
import type { GlobalState } from '...'; // Your root store type

declare module 'react-redux' {
  export declare const useSelector: <T>(selector: (state: GlobalState) => T, equalityFn?: EqualityFn<T>) => T;
}

(The file should be placed in a directory listed in the project's tsconfig.json's typeRoots array – see the docs)

Now calls such as useSelector(state => ...) will automatically treat the state parameter as having the above GlobalState type.

i got TS error when using this

@adamaveray
Copy link

i got TS error when using this

@longb1997 Do you have any further details about the error?

@longb1997
Copy link

longb1997 commented Jan 5, 2023

i got TS error when using this

@longb1997 Do you have any further details about the error?

thanks for the reply, this is error:

hover on useSelector:
Subsequent variable declarations must have the same type. Variable 'useSelector' must be of type '<TState = unknown, Selected = unknown>(selector: (state: TState) => Selected, equalityFn?: EqualityFn<Selected> | undefined) => Selected', but here has type '<T>(selector: (state: GlobalState) => T, equalityFn?: EqualityFn<T> | undefined) => T'. 'useSelector' was also declared here.

and hover on declare:
A 'declare' modifier cannot be used in an already ambient context.

edit: i am using "typescript": "^4.7.4"

@markerikson
Copy link
Contributor Author

@longb1997 : note that we specifically advise against trying to do this!

Instead, follow our docs guidelines for creating "pre-typed" versions of the hooks:

https://redux.js.org/tutorials/typescript-quick-start#define-typed-hooks

@VladimirTi
Copy link

VladimirTi commented May 5, 2023

In case someone is looking for a way how to upgrade big projects to react-redux@8 without changing every useSelector/useDispatch import and usage accross all the project to typed version of it (useAppSelector, useAppDispatch), there is my solution (works on typescript project):

In your tsconfig.json, add next block:

//tsconfig.json
"paths": {
  "react-redux-default": [
    "./node_modules/react-redux"
  ],
  "react-redux": [
    "./src/react-redux-typed"
  ]
},

In react-redux-typed.ts:

//react-redux-typed.ts
import { TypedUseSelectorHook, useDispatch as useDefaultDispatch, useSelector as useDefaultSelector } from 'react-redux-default';

import type { AppDispatch } from './YOUR_PATH';
import type { ApplicationState } from './YOUR_PATH';

export * from 'react-redux-default';

export const useDispatch: () => AppDispatch = useDefaultDispatch;
export const useSelector: TypedUseSelectorHook<ApplicationState> = useDefaultSelector;

That's all. After that, you can leave all useSelector/useDispatch imports and usages as they are, but now they are typed as should be

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

No branches or pull requests

7 participants