6

I have a component class that extends another component, and I'm trying to figure out how to overwrite the type of the state that I'm inheriting from.

Here's an example:

class MyComponent extends React.Component<SomeProps, SomeState> {
  // ...
}

class ExtendedComponent extends MyComponent {
  // How to overwrite SomeState?
  // This component needs to store a different model of state.
}

I need ExtendedComponent to use a different interface for its state, but I can't quite figure out how to accomplish this.


EDIT: Getting somewhere now!

However, now Parent is getting stuffed with all kinds of errors pertaining to state modifications. Here's what I've got so far:

interface ParentProps {
  a: string;
}

interface ParentState {
  b: string;
}

class Parent<P, S> extends React.Component<ParentProps & P, ParentState & S> {
  constructor(props) {
    super(props);

    // Type '{ b: "hello"; }' is not assignable to type 'Readonly<ParentState & S>'
    this.state = {
      b: 'hello',
    };
  }

  aFunction(): void {
    /*
      Argument of type '{ b: "testing"; }' is not assignable to parameter of type '(ParentState & S) | ((prevState: Readonly<ParentState & S>, props: Readonly<ParentProps & P>) => (ParentState & S) | Pick<ParentState & S, "b">) | Pick<ParentState & S, "b">'.
  Type '{ b: "testing"; }' is not assignable to type 'Pick<ParentState & S, "b">'.
    Types of property 'b' are incompatible.
      Type '"testing"' is not assignable to type 'string & S["b"]'.
        Type '"testing"' is not assignable to type 'S["b"]'.
    */
    this.setState({
      b: 'testing',
    });
  }
}

interface ChildProps {
  c: string;
}

interface ChildState {
  d: string;
}

class Child extends Parent<ChildProps, ChildState> {
  constructor(props) {
    super(props);

    // This is what I'm after -- and TypeScript doesn't complain :)
    this.state = {
      b: 'hello',
      d: 'world',
    };
  }
}

EDIT 2:

In retrospect, nearly two years later:

Extending another component class is not recommended by the maintainers of React. This is for good reason, as I had to maintain this approach in a production application which was abysmal! I ended up re-writing a lot of code using higher-order components and custom hooks.

There's a reason that this use-case isn't very supported, avoid creating complication caused by over-engineering your components! Imagine trying to explain your tangled mess of inheritance to another developer.

Just because it sounds cool and you can do it, doesn't mean you should. I highly recommend using functional components, and if you need shared functionality, use higher-order functions, and/or custom hooks.

Kyle
  • 132
  • 8

2 Answers2

3

I'm guessing you don't want to replace the props and state, because you probably need a common interface to make the parent class work. So you could make your parent class generic, and then merge in the additional pieces of props/state.

class MyComponent<P, S> extends React.Component<ParentProps & P, ParentState & S> {}
//                ^ Generic P is for child props, Generic S is for child state

Example in use:

type ParentProps = { a: number }
type ParentState = { b: number }
class MyComponent<P, S> extends React.Component<ParentProps & P, ParentState & S> {}
let parentComponent = <MyComponent a={1} />

type ChildProps = { c: number }
type ChildState = { d: number }
class ExtendedComponent extends MyComponent<ChildProps, ChildState> {}
let childComponent = <ExtendedComponent a={1} c={2} />
Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 1
    Thanks for your answer! Finally getting somewhere :) However, now the `MyComponent` class is getting stuffed full of errors about objects passed to `setState`, and `this.state =` declaration in the `constructor` being incompatible with type `ParentState & S`. Do I have to cast the state modifications? Check out the updated answer. – Kyle Aug 08 '19 at 22:12
  • I uh, don't know... You tried using hooks instead? :P – Alex Wayne Aug 08 '19 at 23:45
  • Nah, there's a lot more to these classes, using functional components isn't an option for me. Thanks for your help, I'll do some more research on Generics and will update the post with any findings. :) – Kyle Aug 08 '19 at 23:55
0

For anyone searching for this and being stuck at the "EDIT 2" problem, one workaround is to spread the previous state in your setState() call like this.

this.setState(prevState => { return { ...prevState, foo: newFoo }});

It's required so that no matter what is in the S template, it will get spread and won't be overwritten (and with that you can also make generic the Extended component and inherit from that and so on).

You also, if you want to set the state in the constructor, you can do

super(props);
this.state = {
   ...this.state, 
   foo: newFoo
};

With that, the state initialized in the parent class will remain. You can also just override it if you know every fields of the parent's state.

Malorke
  • 53
  • 1
  • 6