-
Notifications
You must be signed in to change notification settings - Fork 30.5k
[react]: improve props and state type safety on Component #26813
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]: improve props and state type safety on Component #26813
Conversation
types/react/test/index.ts
Outdated
} | ||
|
||
class BadlyInitializedState extends React.Component<Props, State, Snapshot> { | ||
// $ExpectError |
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.
@Hotell Thank you for submitting this PR! 🔔 @jgoz @dmitryrogozhny @eelco @ghotiphud @schwers @michael-yx-wu @willisplummer @smvilar @sulf @incleaf @pepaar @stephenjelfs @ilivit @bolatovumar @ngbrown @theigor @alitaheri @herrmanno @DaIgeb @allienna @schlesingermatthias @InsidersByte @artyomsv @dan-j @minodisk @samwalshnz @johnnyreilly @Methuselah96 @bbenezech @pzavolinsky @digiguru @ericanderson @morcerf @tkrotoff @DovydasNavickas @onigoetz @theruther4d @guilhermehubner @ferdaber @jrakotoharisoa @forabi @crohlfs @nicolas-schmitt @pjo256 @robessog @tbayne @cdeutsch @rosskevin @varHarrie @bradleyayers @paustint @screendriver @SupernaviX @KieranPeat @martinnov92 @radziksh @andrewBalekha @smrq @Rogach @royxue @KoalaHuman @uncovertruth @MartynasZilinskas @cameron-mcateer @TiuSh @danielasy @rhysd @m0a @jacobbaskin @jnbt @suniahk @iRoachie @j-fro @gazaret @timwangdev @salim7 @jemmyw @NoHomey @charlesrey @mykter @bsurai @kaoDev @guntherjh @wasd171 @szabolcsx @kraenhansen @Stevearzh @mgoszcz2 @alihammad @mfal @danilobjr @fabiopaiva @FaithForHumans @KurtPreston @timc13 @patrickrgaffney @mthmulders @rapmue @ZheyangSong @richbai90 @caspeco-dan - please review this PR in the next few days. Be sure to explicitly select If no reviewer appears after a week, a DefinitelyTyped maintainer will review the PR instead. |
Uff again a lot of errors in 3rd party definitions dependant on this typings, which exposes errors in those ( which is good ! ). Although it's sad to see that react types are not very "strict" and now when we wanna improve things... a lot of refactoring outside is needed 🧐 |
@Hotell The Travis CI build failed! Please review the logs for more information. Once you've pushed the fixes, the build will automatically re-run. Thanks! |
I've updated all react dependant libraries... |
props: Readonly<{ children?: ReactNode }> & Readonly<P>; | ||
state: Readonly<S>; | ||
readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>; | ||
readonly state: null | Readonly<S>; |
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.
Should this instead be:
class Component {
readonly state?: ReadOnly<S>;
}
React itself does not instantiate state on its own (not even to null
), and this more accurately depicts what actually happens (a consumer does not always have to instantiate this.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.
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.
Ooh! 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.
Looks like instantiating state with null creates an issue when one sets state in a constructor.
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.
this is very incorrect. state shouldn't be union with null.
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.
MAYBE you can give the generic of stats a default value of null. no more than 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.
Looks good! Thank you for contributing, you should consider adding yourself as a maintainer to this package! 😄
haha I've added myself already here #26545 but sure can do that for this one :) cheers |
A definition owner has approved this PR ⭐️. A maintainer will merge this PR shortly. If it shouldn't be merged yet, please leave a comment saying so and we'll wait. Thank you for your contribution to DefinitelyTyped! |
The implication of this change is that component classes will need to override their definition of |
Yup, which exposes runtime errors during compile. Like accessing Also this enforces to follow best practices/design patterns :
|
@Hotell Unfortunately, this pull request currently has a merge conflict 😥. Please update your PR branch to be up-to-date with respect to master. Have a nice day! |
- fix(react): remove failing expectation on TS 2.6 - chore(react): add myself to contributors
081040c
to
98f3e2f
Compare
@Hotell I found this PR after getting the latest type definitions and getting bombarded with a billion false alerts about state might be null. |
That's not entirely correct :) you can set it from within class property initializer as well, which Is described in migration docs of this PR already :) class MyComponent extends React.Component<MyComponentProps, MyComponentState> {
readonly state: MyComponentState = {someValue: this.props.someValue || ''};
} |
@Hotell Thanks for the clarification. I deleted my comment after I realized others had already said what I was saying. |
With this change and the recommended way of creating the property, it is easy to forget the export interface IMyComponentState {
x:string;
}
export default class extends React.Component<{}, IMyComponentState> {
public readonly state = {
x: 'hi',
};
public componentDidMount() {
this.state.x = 'yo';
} With the constructor initialization style, I was able to be type-safe and avoid this. In fact, I wish TypeScript forbade redefining the property because it effectively allows you to violate patterns which the super class is trying to enforce. Is there any solution to this? I do like how creating the definition removes the |
Please think more carefully before instantiating some change like this null State again - not only is it breaking for pretty much everyone worldwide, but it is also explicitly against the React documentation. If you're dead set on keeping it, then might I suggest getting a job on the React team and getting official support for screwing them over? You cannot fix a simple problem by forcing a complicated partial solution that is not supported (or even referenced) in any official documentation, especially when it has worldwide consequences. There is a reason that most JS packages use SemVer - and this sort of debacle is a prime example of not understanding it at all - https://semver.org/ |
@Rycochet your rebuke is unnecessary, and your comments about semver leads me to believe you have skipped over much of the discussion including the current limitations of this repo's release process. There were good intentions here and no one was out to inflict pain on others. |
You can say what you want about type safety but this kind of change is unacceptable in a patch version. It shouldn't even be an argument - the old pattern is actively encouraged in official React docs. |
@helmutschneider everyone agrees. Please read about semver limitations above. |
Another approach worth considering is using getDerivedStateFromProps and returning your initial state if null. |
I should note that I think marking state as readonly is the right move but that the null is wrong |
I've submitted PR to revert this #26946 Sorry everyone for trouble that this caused 😥🤒 We should introduce these changes with BC version bump later on one Pro tip/closing words: if something breaks, you can always rollback your version of particular package without giving middle fingers and hating on person responsible for that. That won't help anyone and might discourage person who is investing his time for free to make better oss. We are all in this together, so please try to be nice and constructive. Peace ✌️ |
@rosskevin You're right, I was annoyed. @Hotell I want to apologise for my tone (won't edit above as it was said) - yes, I was somewhat annoyed at the amount of time I had to spend tracking it down, then seeing it was such an annoyingly small change in the midst of a whole load of worthwhile and wanted changes. I'm sorry - and hope you can have a good day despite all the "angry" aimed at you! So (after eating and thinking) - I think that this (null State, readonly is correct) is a change that should get in though not necessarily in this form, but going back to the whole semver discussion - it should be queued up for the next React major change - and should get some involvement from them so that it actually gets supported in the example code. Having said that, it feels like something that needs thinking about in the best way to do it. Having an initialiser property doesn't help unless you have static values (even if it's via function call), and having to state the type again to override the I'm wondering if anyone can think of a better way for it to be done - I generally keep up with the TS updates, but only have my eye on about a dozen open TS issues, and none of them are relevant... |
Please people:On this specific issue
About Semver (AFAIK, I'm not a DT maintainer)
|
@bbenezech One thing to point out on your comment there - Typescript should be able to take Javascript and (provided that good practices are in order) should have no problems with it. That one specific change broke every single piece of React example code that dealt with State and forced extra code to handle it that's not part of the React docs. The change needs to come from that end, or at the very least be kept on the sidelines till React has a major version bump so it's not breaking anyone who uses the default Unfortunately your link to hackermoon loses a lot of its influence when it's got such bad code examples as this: class Foo extends Component {
myHandler = this.myHandler.bind(this);
myHandler() {
// some code here that references this
}
...
} Instead of this: class Foo extends Component {
myHandler = () => {
// some code here that references this
}
...
} Bind is equally dead (unless you have a valid use for it, and that isn't one). It also refers to |
As someone quite new to this community with only a few weeks of React and TypeScript experience, I want to say that this whole discussion is quite an eye-opener. My partner and I were completely baffled by why our code suddenly stopped working. We spent two days hunting through our While I appreciate the type-safety this change was meant to encourage (and I especially appreciate @Hotell's examples of best practice patterns above), I am very glad to see it being reverted in #26946. I fear there are many other folks in a boat similar to ours, still pulling their hair out wondering what they did wrong and why none of the React examples work any longer. The sooner a version of For myself, I wonder if anyone here has advice about how to best follow the development of |
Hi,
|
@Hotell Do you find this to be a major issue with "state as a property" approach: One can completely forget to add State as a generic parameter. At that point, passing anything into type State = {
dismissed: boolean
}
// We forgot to pass State as a superclass generic parameter
export class Alert extends React.Component<Props> {
public readonly state: State = {
dismissed: false,
}
private onClick = () => {
// Anything goes!
this.setState({
whatever: 123,
}) Maybe this can be fixed by changing the default for Interested in hearing your thoughts |
…structor. Updating @types/react to 16.4.4 in light of these two Typescript issues: * DefinitelyTyped/DefinitelyTyped#26813 * DefinitelyTyped/DefinitelyTyped#26912
…structor. Updating @types/react to 16.4.4 in light of these two Typescript issues: * DefinitelyTyped/DefinitelyTyped#26813 * DefinitelyTyped/DefinitelyTyped#26912
Based on this [PR](DefinitelyTyped/DefinitelyTyped#26813) we don't have to put `readonly` for our states, because `React.Component<P,S>` already marks them as immutable.
@@ -281,13 +282,18 @@ declare namespace React { | |||
// tslint:disable-next-line:no-empty-interface | |||
interface Component<P = {}, S = {}, SS = any> extends ComponentLifecycle<P, S, SS> { } | |||
class Component<P, S> { | |||
constructor(props: Readonly<P>); |
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.
Will remove the Readonly
wrapper in #69140 since it doesn't seem to do anything.
npm test
.)npm run lint package-name
(ortsc
if notslint.json
is present).Changing definition:
Provide a URL to documentation or source code which provides context for the suggested changes: <>
Increase the version number in the header if appropriate.
If you are making substantial changes, consider adding a
tslint.json
containing{ "extends": "dtslint/dt.json" }
.This makes props readonly as they are, so consumer will be notified on compile time
state needs to be initialised otherwise accessing it or setting it directly will lead to compile error ( which will error on run time as well )
Migration docs
This PR exposes programatic/compile type errors when handling with state. Till know you got a false assumption about using generics for defining state which was not completely correct.
Something like this was allowed before:
Which has few issues:
So overall 👉no explicit state === error prone code
1. migrate to state initialization via class property
Setting state from within constructor manually is not a good practice, especially not when we have transpiler ( TS ), it also forces you to write unnecessary boilerplate:
for more info check https://medium.freecodecamp.org/react-binding-patterns-5-approaches-for-handling-this-92c651b5af56 - last bullent point 5.)
or (my preference YMMV !) consider extracting initial state to constant, and define type State from implementation 😎👌
2. I need some state initialization logic, what should I do?
A good practice is to extract any initialization logic to pure function ( which is easy to test + other benefits ). You can define it as a static method if you wish as well.
3. OMG what have you done dude, I don't have time for this, gimme quick fix
Although I highly recommend to spent 2 seconds more and use what is described in 1.), you can do a quick fix by defining state ( to let know Compiler that state is there and it's gonna be initialised within constructor )
type Props = {} type State = { who: string } class MyCmp extends Component<Props, State> { + state: State constructor(props:Props){ super(props) this.state = { who: 'Morpheus' } } }