When initializing a component's state to null
outside of the component's constructor, the state has the type never
in the render function.
However, when the state is initialized in the constructor, the state has the correct type.
According to most questions on StackOverflow about the two ways to initialize state (in babeled JS), these two methods should be equivalent. However, in Typescript, they are not. Is this a bug or the intended behavior?
import * as React from "react";
import * as ReactDOM from "react-dom";
interface Person {
name: string;
address: string;
}
interface Props {
items: Person[];
}
interface State {
selected: Person | null;
}
class PersonSelector extends React.Component<Props, State> {
// DOES NOT WORK:
state = {
selected: null
};
constructor(props: Props) {
super(props);
// WORKS:
// this.state = {
// selected: null
// };
}
handleClick = (item: Person) => {
this.setState({
selected: item
});
};
render() {
const { selected } = this.state;
let selectedLabel = <div>None selected</div>;
if (selected) {
selectedLabel = <div>You selected {selected.name}</div>;
}
return (
<div>
{selectedLabel}
<hr />
{this.props.items.map(item => (
<div onClick={() => this.handleClick(item)}>{item.name}</div>
))}
</div>
);
}
}
const people: Person[] = [
{ name: "asdf", address: "asdf asdf" },
{ name: "asdf2", address: "asdf asdf2" }
];
document.write('<div id="root"></div>');
ReactDOM.render(
<PersonSelector items={people} />,
document.getElementById("root")
);
Here is the sample code on CodeSandbox: https://codesandbox.io/s/10l73o4o9q
setState function or the updater function returned by the React. useState() Hook in class and function components, respectively. State updates in React are asynchronous; when an update is requested, there is no guarantee that the updates will be made immediately.
One way is to initialize the state is in the constructor. As we discussed earlier constructor is the first method to be called when React instantiates the class. This is the perfect place to initialize the state for the component because the constructor is called before the React renders the component in the UI.
If we were to mutate state directly, not only would we lose this functionality, but since there is no reference point, adding to or changing the value of an existing element is still going to equal it's previous state.
To return nothing from a React component, simply return null . When null is returned from a React component, nothing gets rendered.
According to most questions on StackOverflow about the two ways to initialize state (in babeled JS), these two methods should be equivalent. However, in Typescript, they are not.
They are different in TypeScript because assigning state
in the class body (not in the constructor) declares state
in PersonSelector
, overriding the declaration in base class React.Component
. In TypeScript, overriding declaration is allowed to have different, more strict type, one-way compatible with the type of the same property in the base class.
When initialized without type annotation, this type is determined from the type of the value:
class PersonSelector extends React.Component<Props, State> {
// DOES NOT WORK:
state = {
selected: null
};
You can see that type of the state
is {selected: null}
, as expected. It becomes never
in this code
const { selected } = this.state;
let selectedLabel = <div>None selected</div>;
if (selected) {
because inside if
statement, the type of selected
is narrowed, using the information that selected
is true
. Null can never be true, so the type becomes never
.
As suggested in other answer, you can annotate State
explicitly when initializing in the class body
class PersonSelector extends React.Component<Props, State> {
state: State = {
selected: null
};
Update to clarify how the initialization in the class body is different from assigning in the constructor
When you set state
in the constructor
constructor(props: Props) {
super(props);
this.state = {
selected: null
};
}
you are assigning the value to state
property that already exists as it was declared in the base class. The base class is React.Component<Props, State>
, and state
property there is declared to have State
type, taken from the second generic argument in <Props, State>
.
Assignments do not change the type of the property - it remains State
, regardless of the value assigned.
When you set state
in the class body it's not mere assignment - it's a declaration of class property, and each declaration gives a type to the declared entity - either explicitly via type annotation, or implicitly, inferred from the initial value. This typing happens even if the property already exists in the base class. I can't find anything in the documentation that confirms this, but there is github issue that describes exactly this behavior, and confirms that sometimes it goes against the developer's intent (no solution implemented in the language so far).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With