Vincent Newkirk
Vincent Newkirk

Reputation: 194

Typescript & ReactJS How To Dynamically Set State

I have this interface defined as my state:

interface State {
  id: string;
  name: string;
  description: string;
  dimensionID: string;
  file: File | null;
  operator: string;
  isFormValid: boolean;
  filename: string;
};

I have a simple on change handler:

  update = (event: React.FormEvent<HTMLInputElement>): void => {
    const { name, value } = event.currentTarget;
    this.setState({ [name]: value });
  };

However, this error gets thrown:

Error:(109, 19) TS2345: Argument of type '{ [x: string]: string; }' is not assignable to parameter of type 'State | Pick<State, "id" | "name" | "description" | "dimensionID" | "file" | "operator" | "isFormValid" | "filename"> | ((prevState: Readonly<State>, props: Readonly<Props>) => State | Pick<...>)'.
  Type '{ [x: string]: string; }' is not assignable to type '(prevState: Readonly<State>, props: Readonly<Props>) => State | Pick<State, "id" | "name" | "description" | "dimensionID" | "file" | "operator" | "isFormValid" | "filename">'.
    Type '{ [x: string]: string; }' provides no match for the signature '(prevState: Readonly<State>, props: Readonly<Props>): State | Pick<State, "id" | "name" | "description" | "dimensionID" | "file" | "operator" | "isFormValid" | "filename">'.

My question is: How can I set state dynamically like how my on change handler tries? This handler is used for different parts of the form so I won't know which key of state I will need to update.

Update: SOLUTION From this post

  update = (event: React.FormEvent<HTMLInputElement>): void => {
    const { name, value } = event.currentTarget;
    this.setState(prevState => ({
      ...prevState,
      [name]: value,
    }))
  };

Works!

Upvotes: 10

Views: 11011

Answers (4)

MenyT
MenyT

Reputation: 2225

Most prefered solution casting.

this.setState({[name]: value} as {[P in keyof State]: State[P]})
// or
this.setState({[name]: value} as Pick<State, keyof State>)

setState with spread previous state

this.setState((prevState) => ({ ...prevState, [name]: value }));

This doesn't check the types. It's just a quick workaround for anyone who is stuck transpiling.

this.setState<never>({[name]: value})

Related links

React setState

Github Tim Roes comment

Upvotes: 3

Joe Gasewicz
Joe Gasewicz

Reputation: 1485

You can use the in keyof keywords to deconstruct the key / value from the IState object.

Create your React state:

interface IState { "email" : string; }

Create a form type that deconstruct the key as K and value as T[K]:

type FormState<T> = { [K in keyof T]: T[K] };
// T represents your IState
// to get the key use: [K in keyof T]
// to reference the value use: T[K]

Then pass the IState as a generic type argument to FormState type:

FormState<IState>

Now you can update the state, casting it to the above FormState:

this.setState({ [formStateItem]: (e.target.value as any) } as FormState<IState>);

In a handler it might look like this:

handleChange = <T, K extends keyof IState>(formStateItem: K) => {
    return (e: ChangeEvent<HTMLInputElement>) => {
        this.setState({ [formStateItem]: (e.target.value as any) } as FormState<IState>);
    }
};

And finally you can use in you JSX like this:

onChange={this.handleChange("email")

Upvotes: 0

tottomotto
tottomotto

Reputation: 2352

The constant name is widened to string. Which version of TS are you using?

You cast it like this, but it's not a good practice though:

this.setState({
  [name]: value
} as Pick<State, keyof State>);

Bug related to this issue:

Upvotes: 2

Alfred Ayi-bonte
Alfred Ayi-bonte

Reputation: 775

eg.

update = (name: string, event: React.FormEvent<HTMLInputElement>): void => {
    const { value } = event.currentTarget;
    this.setState({ [name]: value });
  };

<input onChange={this.update.bind(null, 'first_name')} value={this.state.first_name} />

Upvotes: 1

Related Questions