Edited at

[React] 外からも入力値を上書きできるInput (getDerivedStateFromProps 版)


背景

ぷやんです、React結構好きです。

でもInputはちょっと扱いにくいなぁと思います。

Controlled Componentにすると入力値はstateで管理することになるけど

時には外から入力値をバシッと入れたい時もあります。

というようなことを

componentWillReceivePropsを駆使して実現してたわけですが

componentWillReceivePropsが廃止されるというじゃないですか?

仕方ないのでgetDerivedStateFromPropsを使って作り直しました。

これはその備忘録です。

※コードはTypeScriptです。


先に結論

入力もできるし、外から値を上書きもできるInputを作りたかった。


完成したコード

import * as React from 'react';

/** Props */
export interface IProps {
defaultValue: string
}

/** State */
interface IState {
value:string,
defaultValue:string
}

export default class Text extends React.Component<IProps, IState>
{
constructor(props:IProps) {
super(props);
this.state = {
value:props.defaultValue,
defaultValue:props.defaultValue
}
}

static getDerivedStateFromProps(nextProps:IProps, prevState:IState):IState|null
{
const isChangeProps = (prevState.defaultValue !== nextProps.defaultValue);
const isNeedChange = (nextProps.defaultValue !== prevState.value);
if (isChangeProps && isNeedChange) {
return {
value:nextProps.defaultValue,
defaultValue:nextProps.defaultValue
};
}
return null;
}

render() {
return (
<input type="text"
value={this.state.value}
onChange={this.onChange.bind(this)}
/>
)
}

onChange(e:React.ChangeEvent<HTMLInputElement>) {
this.setState({
value: e.target.value
})
}
}


以下は解説


とりあえずControlled Component

state.valueで入力値を管理

import * as React from 'react';

/** Props */
export interface IProps {
}

/** State */
interface IState {
value:string
}

export default class Text extends React.Component<IProps, IState>
{
constructor(props:IProps) {
super(props);
this.state = {
value:""
}
}

render() {
return (
<input type="text"
value={this.state.value}
onChange={this.onChange.bind(this)}
/>
)
}

onChange(e:React.ChangeEvent<HTMLInputElement>) {
this.setState({
value: e.target.value
})
}
}


初期値を設定できるようにする

入力はできるけど、デフォルト値を設定したいことあるある。

import * as React from 'react';


/** Props */
export interface IProps {
+ defaultValue: string
}

/** State */
interface IState {
value:string
}

export default class Text extends React.Component<IProps, IState>
{
constructor(props:IProps) {
super(props);
this.state = {
- value:""
+ value:props.defaultValue
}
}
...
}

propsdefaultValueなるものを追加

constructorstateの初期値に設定しました。

初期値が設定されています。

しかし残念なことにこれは本当に本当の最初にしか効きません。

constructorで設定してますからね

コンポーネントが生成されたときに1回だけ初期値が設定されますが

そうじゃない、そうじゃないんだ。

ここで今まではcomponentWillReceivePropsを使って

props変わったんやろ?判定をしていたんですが

廃止されてしまうらしいので、getDerivedStateFromPropsを使ってみたいと思います。


getDerivedStateFromPropsを使う

// ...省略

export default class Text extends React.Component<IProps, IState>
{
// ...省略

static getDerivedStateFromProps(nextProps:IProps, prevState:IState):IState|null
{
const isNeedChange = (nextProps.defaultValue !== prevState.value);
if (isNeedChange) {
return {
value:nextProps.defaultValue,
};
}
return null;
}
// ...省略

}

getDerivedStateFromPropsを追加しました。

staticなので頼れるのは引数に渡されるnextPropsprevStateだけです。

戻り値はstateに設定したいオブジェクトを返せばいいそうです。

stateを変更する必要がない場合はnullを返します。

今回はprops.defaultValueが変更されたら

入力値(state.value)を変更したかった

要するに



  • state.value(入力値)


  • props.defaultValue(外から設定された値)

この2つが違ってたら

強制的にstate.valueを変えちまえばいいだろというコードにしました。

いけたわぁ!と思ったらいけてねぇぇ!!

defaultPropsを変えたら入力内容が変わるようになったものの

普通の入力ができなくなった。

どうやらgetDerivedStateFromProps

propsが変わった時だけじゃなくconstructorの後と

コンポーネントの更新時はとにかく呼ばれるらしい

要するに、ずっとprops.defautValueがInputの入力値にセットされる。

<input value={this.props.value} />してた頃を思い出したわ。


さてどうしよう

そもそも、普通に入力されてstate.valueが変更されるときはスルーしたい。

props.defaultPropsが変更されたかどうかを知りたい。

しかしgetDerivedStateFromPropsの引数には

前回のPropsが渡って来ない。

stateに覚えさせるか...


stateでdefaultValueの値を覚えさせてなんとかする

import * as React from 'react';


/** Props */
export interface IProps {
defaultValue: string
}

/** State */
interface IState {
value:string,
+ defaultValue:string
}

export default class Text extends React.Component<IProps, IState>
{
constructor(props:IProps) {
super(props);
this.state = {
value:props.defaultValue,
+ defaultValue:props.defaultValue
}
}

static getDerivedStateFromProps(nextProps:IProps, prevState:IState):IState|null
{
+ const isChangeProps = (prevState.defaultValue !== nextProps.defaultValue);
const isNeedChange = (nextProps.defaultValue !== prevState.value);
- if (isNeedChange) {
+ if (isChangeProps && isNeedChange) {
return {
value:nextProps.defaultValue,
+ defaultValue:nextProps.defaultValue
};
}
return null;
}

// ...省略
}

できたー

これこれ、これがやりたかったんですわ。

stateprops.defaultPropsを保持させておいて

props.defaultPropsが変わった時のみという判定ができるようにしました。

そのためにstateにプロパティを増やすのもなんかなぁ

という気持ちにはなったもののとりあえずできたので

とりあえずめでたしめでたし。