-
-
Notifications
You must be signed in to change notification settings - Fork 15.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
Revert adding the store as a return type to replaceReducer #3772
Revert adding the store as a return type to replaceReducer #3772
Conversation
Deploy preview for redux-docs ready! Built with commit b8fc8a9 |
test/typescript/enhancers.ts
Outdated
const store = createStore(wrapReducer(reducer), wrappedPreloadedState) | ||
return { | ||
...store, | ||
replaceReducer(nextReducer: Reducer<S, A>) { | ||
store.replaceReducer(wrapReducer(nextReducer)) | ||
} | ||
} |
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 change in types actually caught a bug in one the tests where we weren't re-wrapping the reducer previously.
I dunno if @cellog is around, but if he is, did he want to take a look at this? |
I'll try to take a look. |
@cellog I took out some unnecessary changes that I'll put in a separate PR in order to keep this PR as focused as possible. My apologies if you've already started reviewing. |
I don't have time right now to do the fix, but #3767 assumes it is not possible to change the type of the store when it actually is possible. All we have to do is change |
@cellog That sounds really cool, however I'm having trouble getting an example working. Can you help? I'm not seeing much online about it. It seems like within the type guard, the type is now Code interface Containter<T> {
getValue(): T;
changeType<NT>(newValue: NT): this is Containter<NT>;
}
function createConatiner<T>(value: T): Containter<T> {
let savedValue = value;
return {
getValue() {
return savedValue;
},
changeType<NT>(newValue: NT) {
savedValue = newValue as unknown as T;
return true;
}
}
}
const originalContainer = createConatiner(5);
if (originalContainer.changeType('testing')) {
const value = originalContainer.getValue();
// This should be a `string` now right?
const valueShouldBeString: string = value;
}
const value = originalContainer.getValue();
// I guess this is still a number?
const valueStillANumber: number = value; Output"use strict";
function createConatiner(value) {
let savedValue = value;
return {
getValue() {
return savedValue;
},
changeType(newValue) {
savedValue = newValue;
return true;
}
};
}
const originalContainer = createConatiner(5);
if (originalContainer.changeType('testing')) {
const value = originalContainer.getValue();
// This should be a `string` now right?
const valueShouldBeString = value;
}
const value = originalContainer.getValue();
// I guess this is still a number?
const valueStillANumber = value; Compiler Options{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"useDefineForClassFields": false,
"alwaysStrict": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"downlevelIteration": false,
"noEmitHelpers": false,
"noLib": false,
"noStrictGenericChecks": false,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"preserveConstEnums": false,
"removeComments": false,
"skipLibCheck": false,
"checkJs": false,
"allowJs": false,
"declaration": true,
"experimentalDecorators": false,
"emitDecoratorMetadata": false,
"target": "ES2017",
"module": "ESNext"
}
} Playground Link: Provided |
I have been spending some time thinking through this, and experimenting. Here is what I have found.
Regarding #2, let's examine what the purpose of
There is another standard method that is more common for augmenting store state in Redux: store enhancers. A store enhancer may extend the state shape or the store shape. The existing code makes strict typing of store enhancers through inference possible, this PR strips out that possibility. Let's imagine the following scenario:
{
router: {...},
state: {
routing: {...},
// user-defined state
}
// redux functions
} When the user calls {
router: {...},
state: {
routing: {...},
// previous user-defined state
// plus the new form widget
form: {...},
}
// redux functions
} This is all perfectly valid javascript. Note that the store enhancer additions to the store shape and to the state shape persist through a How do we represent this in typescript? If we don't pass in extra fields to the store interface, this is no longer possible to represent, as It also presents a dilemma, which you have stumbled across: how do we tell typescript that the original store variable that The current code will preserve the typing and also correctly infer the propagation of the store enhancer chain through It is true that we will have to use an unsafe cast to continue to use the original store declaration, this change will not propagate automagically throughout the app. But let's look at how you might make this work. Let's imagine that you are using the variable You call Now, you will need to scatter store.replaceReducer(newReducer);
const newStore = (store as unknown as Store<NewState, NewA>); and then using const newStore = store.replaceReducer(newReducer); and requires using a very unsafe double type cast. In the end, this is unlikely to be a good solution, because how do we know for certain which version of the store shape is in use? The answer I see is a different approach: using type guards at the point the store is used. If you have code that relies upon strict typing to differentiate between two different possible store shapes, you should write 2 type guards: function isStore1<S extends Store>(store: S): store is S & StoreShape1 {
return store.getState().shape1 !== undefined
}
function isStore2<S extends Store>(store: S): store is S & StoreShape2 {
return store.getState().shape2 !== undefined
} then, you can use this in the code: if (isStore1(store)) {
// operate on store with shape 1
} else if (isStore2(store)) {
// operate on store with shape 2
} In other words, I don't think redux is the correct surface to fix the problem you are encountering. It is definitely a problem, but there is no generic way to solve your specific issue. However, the code that is in redux that this PR strips does solve the problem of store enhancers that augment the store or state shape in a generic, chainable fashion so that you can use createStore with any store enhancer and still use For these reasons, I don't think this PR is the right solution. I think the right solution is perhaps to add something similar to the explanation above to the documentation. Thanks for being patient, I have zero time to do proper investigations during the week right now with working from home and full-time parenting. Also, thanks for raising this issue and putting so much effort in, I am definitely interested in having further dialog with you, @timdorr and @markerikson about these points. I think it's fair to say all of this is pretty edge case-y for most folks who use redux (we don't use any of this fanciness in my workplace, for instance. The most avant garde stuff we use is thunks, and even that is about to be stripped out in favor of using redux only as the caching layer), so it may be that we 2 are the only people who care about this. Either way, let's see if we can come to a resolution that works for your needs. |
@cellog Thanks for spending the time to think about this some more, I really appreciate it. I'm working on TypeScript-ing The example you provided is very helpful and helps ground the discussion. I agree that having type-safety is the whole point of using TypeScript, and so if we lower ability for the types to catch mistakes, then its not serving its purpose (that's why we have I'm gonna dig into it a little bit more and see what I can come up with. Again, really appreciate the response. |
@cellog Apologies in advance for the length of this response. It seems like there are two concerns: type-safety for the person writing the enhancer and type-safety for the person using the enhancer. Type-safety for the person using the enhancerAs you noted, it seems like the best way to ensure type-safety would be to have type-guards. However, I'm not sure how having With return from
|
P.S. Regardless of how we type it, I agree some documentation would be helpful and I would be willing to write up a section on how to use it on the Usage With TypeScript page. I think there are more situations with the current typing where it's easier to go wrong and for the code to still compile. I think getting rid pf the return type would make it more clear what is going on, would make the user think twice before doing anything rash with For code splitting (which is the other usage for |
I am playing a concert tonight, so can't dive in depth into your explanation, but my initial reaction is that it sounds solid to me. More tomorrow. |
Good luck on your concert, hope it goes well! Sounds like you got enough to keep you busy with working from home and being a full-time parent. Thanks for spending the time to help out here as well. |
Just to chime in here: I'm cool with changing the signature of the API, given that this will release in a major. However, now that I think about it in this context, I don't think a return from Redux treats the store as an instance object, not a factory. That is, the other functions on a Put another way, it feels not too far off from just being a wrapper around Also, there is a subtle issue with subscribers, as I'm not sure of the best way to resolve those issues, but I figure I should voice them now before we get too far. |
It's worth noting that adding the return type hasn't been released yet. So this is actually reverting a potentially breaking change that would've been released in v5, not making a new breaking change. |
b8fc8a9
to
ebe6915
Compare
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 8edc1ff:
|
@@ -172,7 +170,7 @@ export interface Store< | |||
* | |||
* @returns The current state tree of your application. | |||
*/ | |||
getState(): S | |||
getState(): ExtendState<S, StateExt> |
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.
Ideally the Store
type would only need to know about S
and A
. Unfortunately this is necessary since replaceReducer
should be able to take the reducer without StateExt
being applied (the same reducer that was passed into createStore
). My preference would be to remove StateExt
altogether since I don't think there's a good practical use-case for it, but short of that, this seems like the necessary solution.
import { createStore, combineReducers } from '..' | ||
|
||
describe('replaceReducers test', () => { | ||
it('returns the original store', () => { | ||
const nextReducer = combineReducers({ | ||
foo: (state = 1, _action) => state, | ||
bar: (state = 2, _action) => state | ||
}) | ||
const store = createStore((state, action) => { | ||
if (state === undefined) return { type: 5 } | ||
return action | ||
}) | ||
|
||
const nextStore = store.replaceReducer(nextReducer) | ||
|
||
expect(nextStore).toBe(store) | ||
}) | ||
}) |
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 removed this test since this was explicitly testing for the behavior that is removed in this PR.
import { combineReducers, createStore } from '../..' | ||
|
||
/** | ||
* verify that replaceReducer maintains strict typing if the new types change | ||
*/ | ||
const bar = (state = { value: 'bar' }) => state | ||
const baz = (state = { value: 'baz' }) => state | ||
const ACTION = { | ||
type: 'action' | ||
} | ||
|
||
const originalCompositeReducer = combineReducers({ bar }) | ||
const store = createStore(originalCompositeReducer) | ||
store.dispatch(ACTION) | ||
|
||
const firstState = store.getState() | ||
firstState.bar.value | ||
// @ts-expect-error | ||
firstState.baz.value | ||
|
||
const nextStore = store.replaceReducer(combineReducers({ baz })) // returns -> { baz: { value: 'baz' }} | ||
|
||
const nextState = nextStore.getState() | ||
// @ts-expect-error | ||
nextState.bar.value | ||
nextState.baz.value |
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.
Removed this test as well since it was written to test the idea that replaceReducer
would return a store with new types, which is no longer the case.
3b97d22
to
1f12c51
Compare
55df1dc
to
ee94ce5
Compare
ee94ce5
to
8edc1ff
Compare
Okay, going to assume that this is good to go now. Thanks for putting up with my delay here! :) |
name: "Revert adding return to replaceReducer"
about: Revert adding return to replaceReducer
PR Type
Does this PR add a new feature, or fix a bug?
Fix a bug.
Why should this PR be included?
This PR resolves #3767 and resolves #3482.
tl;dr You can't change the shape of the store and have static typing at the same time.
This PR reverts most of #3507 and #3524. If someone wants to change the state or actions that a store should operate on, then they'll have to cast the store. This cast should hopefully make it obvious to them that they're doing an operation that would make the types of the original store incorrect.
Note that this resolves the original issue in #3482. You can now replace the reducer with your original reducer and it compiles correctly.
Checklist
Bug Fixes
What is the current behavior, and the steps to reproduce the issue?
When you replace a reducer, it returns a new store with new types for state and action. You can still access the old store which now has the wrong types for store and action.
What is the expected behavior?
The user should be forced to cast the store if they're replacing the reducer with a new reducer that has a different type for state or actions.
How does this PR fix the problem?
It removes the return from
replaceReducer
.