なんかパッと出てこなかったのでメモ
簡単に以下の画像のようなアプリケーションを作成してみます。
デフォルトの数字を親から受け取り、ボタンを押してインクリメントしたりデクリメントしたりリセットしたりするようなものです。
これをrecomposeを使って実装してみます。
recomposeってなに?って方は、以下の記事も参考にしてみてください。
【参考】ReactNativeでHoCとRecomposeを使う | mrsekutの備忘録
問題その1 親コンポーネントのpropsを子コンポーネントに伝達する方法がわからない
AtomicDesignの設計で組んでいくと、親から子へpropsを伝搬させることが多々あります。
そこで、recomposeと組み合わせたときにも親から子へ流す方法をしる必要がありました。
今回の場合は、以下のように親から受け取ったdefaultNum
を子コンポーネントで表示させる方法です。
解決策
以下のようなめちゃくちゃシンプルな親子のコンポーネントを考えます。
親コンポーネント
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
export default class ParentComponent extends React.Component<Props, State> { constructor(props: Props) { super(props); } public render() { const data = 'hello recompose'; return ( <Container> <CounterBoard defaultNum={5} /> // ここで子にdataを渡したい </Container> ); } } |
子コンポーネント
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import { compose } from 'recompose'; interface CounterBoardProps { defaultNum: number; } const CounterBoardContainer = compose(); const CounterBoardPresenter = ({ defaultNum }) => ( // ここで受け取る <View> <Heading type="h1">{defaultNum}</Heading> </View> ); const CounterBoard = CounterBoardContainer(CounterBoardPresenter); export default CounterBoard; |
上の例ではrecomposeを使う意味はまったくないですが、例のために仕方なくです・・。
大事なのは、CounterBoardPresenterでいつもと同じように({ defaultNum })
でpropsを受け取っている部分です。
知ってしまえばなんてことないけど、最初はここはprops
と書くべきなのか、defaultNum
と書くべきなのかなどで詰まったりしました。
また、子コンポーネントで受け取る引数は()
の中ではなく、({})
の中に書きます。
こうすることで、指定した名前のprops単体を受け取ることができます。
少し冗長な書き方をするならば、 以下のような書き方でも同じです。
1 2 3 4 5 |
const CounterBoardPresenter = ( props ) => ( <View> <Heading type="h1">{props.defaultNum}</Heading> </View> ); |
問題その2 TypeScriptを使っていると親の方で「子でそれを受け取るやつがないぞ」と言われる
問題その1は解消したのも束の間、compose関数内にちゃんとロジックを書いていくと、以下のようなエラーに遭遇しました。
Property 'defaultNum' does not exist on type 'IntrinsicAttributes &; IntrinsicClassAttributes<Component<{}, ComponentState, any>> & Readonly<{ c...'.
これは、親のParentComponentの方で起こっているエラーです。
親ファイルの中では、defaultNum={5}
というふうにdefaultNum
を渡しているけど、子の方ではそれを受け取る記述がないぞと言われています。
解決策
原因は型です。
recomposeのAPIに型を与えていきます。
以下のコードは型を全く書いていない、エラー出まくりのものです。
簡単のため、withState内で定義している数字の初期値は親propsのものではなく、定数の5としています。(理由は後述)
なので、下のコードではボタンを押しても表示されている数字に変化はありません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
import { compose, withState, withHandlers } from 'recompose'; interface CounterBoardProps {} const CounterBoardContainer = compose( withState('counter', 'updateCounter', 5), withHandlers({ increment: ({ updateCounter }) => () => updateCounter(counter => counter + 1), decrement: ({ updateCounter }) => () => updateCounter(counter => counter - 1), reset: ({ updateCounter }) => () => updateCounter(5) }) ); const CounterBoardPresenter = ({ defaultNum, counter, increment, decrement, reset, ...props }) => ( <View> <Heading type="h1">{defaultNum}</Heading> <IncrementButton onPress={() => { increment(); }}> + </IncrementButton> <DecrementButton onPress={() => { decrement(); }}> - </DecrementButton> <Button onPress={() => { reset(); }}> reset </Button> </View> ); const CounterBoard = CounterBoardContainer(CounterBoardPresenter); export default CounterBoard; |
これらに適切に、型を与えていきます。
今回、型を与えるべきものに以下の4つがあります。
- compose
- withState
- withHandler
- CounterBoardPresenter
エラーの原因はcomposeに型が与えられていないからです。
atomのパワーを借りて、型を確認してみます。ちなみにこれは型定義ファイルを直接読みに行っても同じです。
こんなふうに表示されました。
1 2 3 |
export function compose<TInner, TOutter>( ...functions: Function[] ): ComponentEnhancer<TInner, TOutter>; |
どうやら、「TInner」と「TOutter」に当たる型を定義する必要があるようです。
「TOutter」というのは、outer componentに対する型。
つまり、ここでは、「子コンポーネント」と呼んでいる、CounterBoardPresenterの型のこと。
「TInnerと」いうのは、iner componentに対する型。
つまり、recomposeを使用して作成されたすべてのprops。
ここでは、withStateとwithHandlerを使っており、その中で、counter
という変数や、increment
という関数などを使用していますが、これらの型のことです。
なので、以上を踏まえて型を定義していきます。
型を定義する
まずは、CounterBoardPresenterに対する型、ComponentPropsインターフェースを定義します。
親から受け取るpropsなので、今回はdefaultNumのみです。
1 2 3 |
interface ComponentProps { defaultNum: number; } |
次に、withStateに対する型。WithStatePropsインターフェースです。
1 2 3 4 |
interface WithStateProps { counter: number; updateCounter: (f: (counter: number) => number) => void; } |
少しややこしいですが、updateCounterの型は、「『number型を引数にとり、number型を返す関数』を引数にとり、void型を返す関数」という意味を示しています。
そして同様にして、withHandlerに対する型を定義していきます。
1 2 3 4 5 |
interface WithHandlerProps { increment: () => void; decrement: () => void; reset: () => void; } |
最後にこれらを組み合わせて、先程のTInnerとTOutterに対応する型を作成します。
1 2 3 4 |
// recomposeの内部で使うrecomposeAPIの型 type ComposedProps = WithStateProps & WithHandlerProps; // 上で宣言したものと、自分のものを合わせた型 type CounterBoardProps = ComposedProps & ComponentProps; |
型を加える
型の準備ができたので、適用していきます。
まず、withState関数に型を加えてやります。
1 2 3 4 5 |
withState<WithStateProps, number, 'counter', 'updateCounter'>( 'counter', 'updateCounter', 5 ), |
そしてwithHandler関数にも加えます。
1 2 3 4 5 |
withHandlers<CounterBoardProps, WithHandlerProps>({ increment: ({ updateCounter }) => () => updateCounter(counter => counter + 1), decrement: ({ updateCounter }) => () => updateCounter(counter => counter - 1), reset: ({ updateCounter }) => () => updateCounter(5) }) |
以上を組み合わせると以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
import { compose, withState, withHandlers } from 'recompose'; interface ComponentProps { defaultNum: number; } interface WithStateProps { counter: number; updateCounter: ((f: (counter: number) => number) => void); } interface WithHandlerProps { increment: () => void; decrement: () => void; reset: () => void; } type ComposedProps = WithStateProps & WithHandlerProps; type CounterBoardProps = ComposedProps & ComponentProps; const CounterBoardContainer = compose<CounterBoardProps, ComponentProps>( withState<WithStateProps, number, 'counter', 'updateCounter'>( 'counter', 'updateCounter', 5 ), withHandlers<CounterBoardProps, WithHandlerProps>({ increment: ({ updateCounter }) => () => updateCounter(counter => counter + 1), decrement: ({ updateCounter }) => () => updateCounter(counter => counter - 1), reset: ({ updateCounter }) => () => updateCounter(5) }) ); const CounterBoardPresenter: React.SFC<CounterBoardProps> = ({ defaultNum, counter, increment, decrement, reset, ...props }) => ( <View> <Heading type="h1">{counter}</Heading> <IncrementButton onPress={increment}>+</IncrementButton> <DecrementButton onPress={decrement}>-</DecrementButton> <Button onPress={reset}>reset</Button> </View> ); const CounterBoard = CounterBoardContainer(CounterBoardPresenter); export default CounterBoard; |
以下の質問を見つけてこの解に行き着きました。
【参考】reactjs – Property does not exist on React component when defined with recompose – Stack Overflow
問題その3 recomposeのcompose内で親propsを使う方法
先程は、withState内で、stateの初期値を5と設定しましたが、
ここに親から受け取ったpropsである、defaultNum
の値を入れたいとします。
以下のコードは動きませんが、直感的に書くとこんな感じです。
1 2 3 4 5 |
withState<WithStateProps, number, 'counter', 'updateCounter'>( 'counter', 'updateCounter', defaultNum // コレ! ), |
また、withStateの中のみでなく、withHandlerの中のreset関数の中でも必要です。
解決策
withState
こんなふうにしてやります。
1 2 3 4 5 |
withState< WithStateProps, (x: ComponentProps) => number, "counter", "updateCounter" >( "counter", "updateCounter", ({ defaultNum }: ComponentProps) => defaultNum ) |
詳しくは後述しますが、withStateは第3引数に変数もしくは関数をセットできますが、関数の場合、この関数の引数に親のpropsが入るように実装されているからです。
withHandler
こんなふうにして渡してやります。
1 2 3 4 5 |
withHandlers<CounterBoardProps, WithHandlerProps>({ increment: ({ updateCounter }) => () => updateCounter(counter => counter + 1), decrement: ({ updateCounter }) => () => updateCounter(counter => counter - 1), reset: ({ updateCounter, defaultNum }) => () => updateCounter(() => defaultNum) // これ! }) |
こちらも、ほとんど同じ理由です。
問題その4 withHandlerがよくわからない
よくわかないのは、内部実装がよくわかってないからです。
折角の機会ですので、内部のコードを読んでみます。
と、思ったのですが、解説を書いてみるとめちゃくちゃ長くなってしまったので、別記事にしました。
下記終わり次第、ここにリンクを貼ります。