TypeScriptでHoCを反応させます。 痛みのないタイピング





ReactプロジェクトをTypeScriptに変換することになると、HoC(高次コンポーネント-ラッパーコンポーネント)の作成が最も苦痛を引き起こすとよく耳にします。 今日は、痛みを伴わずに簡単に行う方法を紹介します。 この手法は、TSプロジェクトだけでなく、ES6 +プロジェクトにも役立ちます。



例として、標準のHTMLInputをラップするHoCを使用します。最初の引数では、Eventオブジェクトの代わりにonChangeがテキストフィールドの実際の値を渡します。 このアダプターを実装するための2つのオプションを検討してください:コンポーネントを受け取る関数として、およびラッパーとして。



多くの新参者がこの問題を真っ先に解決します-React.cloneElementを使用して、新しいPropsで子として渡される要素のクローンを作成します。 しかし、これはこのコードをサポートするのに困難をもたらします。 これを二度としないために、この例を見てみましょう。 ES6コードから始めましょう。



//       const onChangeHandler = event => onChange && onChange(event.target.value); export const OnChange = ({ onChange, children }) => { //   ,    //      children const Child = React.Children.only(children); //        props return React.cloneElement(Child, {onChange: onChangeHandler}); }
      
      





子の一意性のチェックとonChange



プロパティの転送を無視する場合、この例をさらに短く書くことができます。



 //       const onChangeHandler = event => onChange(event.target.value); export const OnChange = ({ onChange, children }) => React.cloneElement(children, {...children.props, onChange: onChangeHandler});
      
      





ラッパー関数の外部の内部コンポーネントに渡すためのコールバックを設定することに注意してください。これにより、コンポーネントの各レンダーサイクルで関数を再作成できなくなります。 しかし、TypeScriptについて話しているので、いくつかの型を追加して、次のコンポーネントを取得します。



 import * as React from 'react'; export interface Props { onChange: (value: string) => void; children: JSX.Element; } export const OnChange = ({ onChange, children }: Props) => { const onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => ( onChange(event.target.value) ) const Child = React.Children.only(children); return React.cloneElement(Child, {...children.props, onChange: onChangeHandler}); }
      
      





コンポーネントのProps記述を追加し、onChange:と入力しました。その中に、HTMLInputから渡されたイベントオブジェクトと署名により一致するイベント引数を入力することを示しました。 同時に、外部プロパティでは、onChangeで、イベントオブジェクトではなく最初の引数が文字列であることを示しました。 悪い例は終わりました。それでは先に進みましょう。



HoC



次に、HoCを作成する良い例を見てみましょう。元のコンポーネントをラップして新しいコンポーネントを返す関数です。 これにより、react-reduxパッケージの接続機能が機能します。 これには何が必要ですか? 簡単に言えば、コンポーネントのHoCである匿名クラスを返す関数が必要です。 TypeScriptの重要な問題は、ジェネリックを使用してHoCを強く分類する必要があることです。 しかし、その詳細については、ES6 +の例から始めましょう。



 export const withOnChange = Child => { return class OnChange extends React.Component { onChangeHandler = event => this.props.onChange(event.target.value); render() { return <Child {...this.props} onChange={this.onChangeHandler} />; } } }
      
      





最初の引数は、コンポーネントのインスタンスを作成するために使用されるコンポーネントクラスの宣言です。 renderメソッドでは、変更されたコールバックonChangeおよび他のすべてのプロパティを、変更なしでラップされたコンポーネントインスタンスに渡します。 最初の例のように、renderメソッドの外側のonChangeHandler関数の初期化を削除し、関数インスタンスへのリンクを内部コンポーネントに渡しました。 多少複雑なReactプロジェクトでは、HoCを使用するとコードの移植性が向上します。これは、共通ハンドラーが個別のファイルに移動され、必要に応じて接続されるためです。



この例の匿名クラスをステートレス関数に置き換えることができることに注意してください:



 const onChangeHandler = onChange => event => onChange(event.target.value); export const withOnChange = Child => ({ onChange, ...props }) => <Child {...props} onChange={onChangeHandler(onChange)} />
      
      





ここでは、このコンポーネントのプロパティを取得するステートレス関数を返すコンポーネントクラスの引数を持つ関数を作成しました。 新しいonChangeHandlerを作成する関数は、内部コンポーネントからイベントハンドラーを渡すときにonChangeハンドラーに渡されました。



次にTypeScriptに戻ります。 このようなアクションを実行すると、デフォルトでは渡されたコンポーネントと戻り値はanyタイプになるため、厳密な型指定を最大限に活用することはできません。 厳密モードが有効になっている場合、TSは、関数の引数のいずれかの暗黙的な型に関するエラーをスローします。 さて、入力に取りかかりましょう。 まず、受け取ったコンポーネントと返されたコンポーネントでonChangeプロパティを宣言しましょう。



 //     export interface OnChangeHoFProps { onChange?: (value: string) => void; } //  ,    export interface OnChangeNative { onChange?: React.ChangeEventHandler<HTMLInputElement>; }
      
      





これで、ラップされたコンポーネントが持つべきPropsと、コンポジションから生じるPropsを明示的に示しました。 次に、コンポーネント自体を宣言します。



 export function withOnChangeString<T extends OnChangeNative>(Child: React.ComponentType<T>) { . . . }
      
      





ここでは、特定の署名のonChangeプロパティがプロパティで設定される引数としてコンポーネントが受け入れられることを指摘しました。 ネイティブのonChangeを持つ。 HoCが機能するためには、コンポーネント自体と同じ外部プロパティを既に持っているが、変更されたonChangeを備えたReactコンポーネントをそこから返す必要があります。 これは、式OnChangeHoCProps & T



によって行われます。



 export function withOnChangeString<T extends OnChangeNative>(Child: React.ComponentType<T>) { return class extends React.Component<OnChangeHoCProps & T, {}> { . . . } }
      
      





これで、パラメータとして文字列を取得し、ラップされたコンポーネントを返し、イベントとして引数を返す内部コンポーネントにonChangeを設定することを想定して、コールバックonChangeを受け入れる型付きHoCができました。



React DevToolsでコードをデバッグするときに、コンポーネントの名前が表示されない場合があります。 displayName静的プロパティは、コンポーネント名の表示を担当します。



 static displayName = `withOnChangeString(${Child.displayName || Child.name})`;
      
      





内部コンポーネントから同様のプロパティを取得し、HoCの名前を文字列としてラップしようとしています。 そのようなプロパティがない場合は、ES2015仕様を使用できます。この仕様では、すべての関数のnameプロパティを追加し、関数自体の名前を示します。 ただし、ES5へのコンパイル時にTypeScriptは、関数にこのプロパティがないことを示すエラーをスローします。 この問題を解決するには、tsconfig.jsonに次の行を追加します。



 "lib": ["dom", "es2015.core", "es5"],
      
      





この行を使用して、ES2015、ES5、およびDOM APIの基本セットをコードで使用できることをコンパイラーに伝えました。 HoCの完全なコード:



 export function withOnChangeString<T extends OnChangeNative>(Child: React.ComponentType<T>) { return class extends React.Component<OnChangeHoFProps & T, {}> { static displayName = `withOnChangeString(${Child.displayName || Child.name})`; onChangeHandler = (event: React.ChangeEvent<HTMLInputElement>) => this.props.onChange(event.target.value); render() { return <Child {...this.props} onChange={this.onChangeHandler} />; } } }
      
      





HoCで戦闘の準備ができたので、次のテストを使用して操作をテストします。



 //   Props   HTMLInputElement type InputProps = React.DetailedHTMLProps<React.InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>; //   ,  HTMLInputElement const SimpleInput: React.StatelessComponent<InputProps> = ({...props}: InputProps) => <input className="input" {...props} />; //    HoC' const SimplerInput = withOnChangeString<InputProps>(SimpleInput); describe('HoC', () => { it('simulates input events', () => { const onChange = jasmine.createSpy('onChange'); const wrapper = mount(<SimplerInput onChange={onChange} />); wrapper.find(SimplerInput).simulate('change', { target: {value: 'hi'} }); expect(onChange).toHaveBeenCalledWith('hi'); }); });
      
      





結論として



今日は、ReactでHoCを作成するための基本的なテクニックを見ました。 ただし、実際には1つではなく、2つではなくHoCのチェーン全体が使用されることがあります。 コードをヌードルにしないために、 compose



関数がありますが、次回はそれについて説明します。



プロジェクトのソースコードはGitHubで入手できます 。 ブログを購読してお楽しみに!



All Articles