Monaca Advent Calendar 2017の12日目のエントリです。
MonacaでReact,Redux,OnsenUIを使ってスマホアプリを作っているのですが、
OnsenUIのNavigatorとTabbarを共存させる時にだいぶハマってしまったので、そのことについて書きたいと思います。
問題点
・OnsenUIのNavigatorを使う時、親コンポーネントから子コンポーネントにpropsを伝達する方法がわからない。
・一応伝達の仕方がわかっても、NavigatorとTabbarを共存させようとすると上手くいかない。
一つ目の問題点について
OnsenUIのドキュメントがわかりにくいというのと、英語なので読むの辛かった、そもそも情報が少ないなどのように色々理由はあるのですが、最初はpropsの伝達の仕方が全然わかりませんでした。
【参考】
Navigator React Component – Onsen UI Framework
Onsen UI React拡張を使ったナビゲーションとタブの使い方 | モナカプレス
React + OnsenUI for Reactの基本コンポーネント② (Navigator・Toolbar編) – Qiita
基本的にReduxを使うときはpropsのバケツリレーを避けるためにSpread Attributesを使うかと思いますが、これを書く場所に注意が必要でした。
普段なら、
1 |
<Navigator {...this.props} /> |
のようにかくと思いますが、今回は違います。
1 2 3 4 5 6 7 8 |
<Navigator // {...this.props} ここではない! initialRoute={{ component: PageAlert, props: {...this.props}, // ここ! }} renderPage={this.renderPage} /> |
という風に書くみたいです。
また、Spread Attributesを使わずに一つずつ書くなら以下のような感じです。
1 2 3 4 5 6 7 8 9 |
<Navigator initialRoute={{ component: PageAlert, props: { title: this.props.title }, }} renderPage={this.renderPage} /> |
これでpropsの伝達はできるのですが、これでも2個目の問題は解決しませんでした。
二つ目の問題点について
今回登場するコンポーネントたち
問題はコンポーネントの組み合わせ方にありました。
今回登場するコンポーネントは以下のような感じです。
・mainコンポーネント
monacaでReactテンプレで作るとデフォルトで入っているファイル。親の親、一番親。王様です。基本触らない。
・Navigator
node_modules/react-onsenui/src/components/Mavigator.jsxで定義されているOnsenUIの一コンポーネント。
基本触らない。
・Tabbar
node_modules/react-onsenui/src/components/Tabbar.jsxで定義されているOnsenUIの一コンポーネント。
基本触らない。
タブバーの複数のアイコンがメリケンサックみたいでしょ?
・Aコンポーネント
親の親。おじいちゃんコンポーネント。
ReduxのProviderとcreateStoreを使っています。
1 2 3 4 5 6 7 8 9 10 11 12 |
class A extends React.Component { render() { const store = createFinalStore() return ( <Page> <Provider store={store}> <B {...this.props}/> </Provider> </Page> ) } } |
・Bコンポーネント。
親コンポーネント。お父さんです。
いわゆるContainerComponentでreact-reduxでconnectをしています。
1 2 3 4 |
export default connect( mapStateToProps, mapDispatchToProps )(B) |
・Cコンポーネント
子。赤ちゃんコンポーネントです。
いわゆるPresentationalComponentでReduxを使わないただの部品です。
考えられるアプローチ
ここでは簡単の為に、Tabbarの分岐は2つということにします。(通常は2つ以上かと思います)
画面下のアイコンが2つということです。
そもそもここにいくつかのアプローチがある事自体に気付くのにだいぶ時間がかかってしまいました。
考えてみると6つのアプローチがありました。
1.Reduxをつかわない。
最後の手段です。確かにReduxを使わないと動いているので最悪Reduxなしで書こうと思っていました。
2.Navigatorを使わない。
これも最後の選択肢の一つでした。
これの使い方に問題があるのならコレを使わければよく、何か代替できるもの、例えばreact-routeなどを使うのもありかなと思いました。
3.一番最初にしていた構造です。
Teratailにも質問しました。
プロジェクト作成時にTabbarテンプレで作成し、後からNavigatorを組み込んだ形です。
1回目はちゃんと回るのですが、再描画の際にpropsが上手く伝達しませんでした。
4.Navigatorテンプレで作成した後にTabbarを入れた人の話を参考にしてみました。
これで上手く行ったのですが、ページ遷移を行ってみると、Navigatorの部分からページが変わるので、ページ遷移後にTabbarが消えてしまいました。
5.数撃ちゃ当たるだろう戦法
この辺はもうやけになって狂いながらやってました。
Illegal Constructor
という謎のエラーが出たので泣きそうになりました。
6.最後の選択肢
この辺で整理してみてまだやっていない組み方があることに気づきました。コレが無理なら、「1の方法」にしようと思っていました。
結果的にコレがうまくいきました。
6つ目のアプローチの詳細
一つ一つのファイルの中身を簡単に記しておきます。
redux-formを使っている方はこの記事一番下の【注意】も参考にしてみてください。
main.jsx
触りません。
Tabbarを使うファイル.jsx
Tabbarコンポーネントを記述するファイルです。
例としてこんな感じで書きます。ココまでは普通。
1 2 3 4 |
{ content: <A/>, tab: <Tab label='Alert' icon='fa-bell' /> }, |
Navigatorを使うコンポーネント
注意してください。これはファイルではなくコンポーネントです。
この中でNavigatorを使います。注意するところはNavigatorコンポーネントをPageコンポーネントで括る点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
export default class PageAlertNavi extends React.Component { renderPage(route, navigator) { const props = route.props || {} props.navigator = navigator return React.createElement(route.component, props) } render() { return ( <Page> <Navigator initialRoute={{ component: A, props: {...this.props}, }} renderPage={this.renderPage} /> </Page> ); } } |
後はこれをおじいちゃんコンポーネントの中に入れて、
それをお父さんコンポーネントに入れてconnectします。
また、Tabbarを使った違うページでもNavigatorを使いたいときは同じような構造を書けば実現できます。
まとめ
今回の問題点はOnsenUIのコンポーネントとReduxのコンポーネントの組み合わせ方にありました。
したいことは実現したのですが、なぜその問題が起こったのか、ということに関してはまだ未解決です。
まぁ、動いて良かったです。
我ながら、この記事は大分わかりにくいと思うので、もし疑問点があればこの記事に質問して頂けると結構かと思います。
【追記の注意】
基本的に以上の様な構造で大丈夫だと思うのですが、後にredux-formを追加した時に、少しエラーが出たので変更した部分があります。
親と子の関係は基本的に同じなのですが、
・main.jsxでAppタグをProviderタグで挟む
・App.jsxでAppコンポーネントをconnectする
の2つの操作をすることにより、エラーは解消されました。