Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does null React component state initialization get `never` type?

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

like image 463
Eric Avatar asked Jul 05 '18 23:07

Eric


People also ask

Why does setState not update immediately?

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.

What is the correct way to set the state when initializing the component?

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.

Why should we not mutate States directly in React?

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.

Can a React component return null?

To return nothing from a React component, simply return null . When null is returned from a React component, nothing gets rendered.


1 Answers

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).

like image 113
artem Avatar answered Oct 01 '22 17:10

artem