- 投稿日:2019-07-28T21:35:48+09:00
VirtualDOMのメリット
ReactやVueで採用されている仮想DOM(Virtual DOM)の何が良いの?って話。
そもそもDOMとは?
DOMはXML形式の文書をツリー構造で表現したものです。このDOMを参照することで、各要素にアクセスすることができ、見た目やコンテンツを変更することができます。
とりあえず、「WEBページを表示するのに必要な設計書みたいなもの」と思ってもらえると良いと思います。
仮想DOMの説明の前に、ブラウザのレンダリングの流れについて
仮想DOMのメリットを知るためには、ブラウザのレンダリングの流れを知る必要があります。
ブラウザはページを描画するまでに以下の処理を行っています。
- Parse HTML/CSSを解析
- DOM, CSSOM DOMツリーとCSSOMツリーを組み合わせて、レンダーツリー作成
- Layout 描画する位置、サイズの決定
- Paint 描画処理
- Composite 各レイヤーを重ねる
DOM操作を行うと、レンダーツリーの更新が発生し、再度レイアウトとペイント処理が走ります。
これら「レイアウト」と「ペイント」処理が負荷が高い処理と言われています。
そのため、描画速度を上げるためには、一般的に不要なDOM操作を減らすという手段が取られています。レンダリングの流れについて、より詳しく知りたい方は、以下の記事を参考にしてください。
- ブラウザの仕組み: 最新ウェブブラウザの内部構造
- ブラウザレンダリング入門〜知ることで見える世界〜仮想DOMとは?
実際のDOMと対となる、構造体。
先程書いたように、DOM操作を行うたびにレンダーツリーの更新が発生し、再度レイアウトとペイント処理が走ります。
この描画時の負荷を抑えてくれる仕組みが、仮想DOMです。
【本題】仮想DOMのメリット
仮想DOMを使うことで、以下のようにDOM操作を最小限に抑えることができます。
- DOMと対になる仮想DOMを定義し、ページが変化するときには、まず仮想DOMを変更
- 仮想DOMはブラウザレンダリングとは切り離されているため、変更しても影響はない
- 仮想DOMの差分をチェックし、変更部分のDOMのみを変更
- 最小限のDOM操作でページを変化させることができる
まとめ
仮想DOMとは、DOMと対になる構造体で、ReactやVueは、仮想DOMの差分をチェックし、DOMの再構築を最小限に抑えてくれる!
- 投稿日:2019-07-28T17:20:48+09:00
redux-saga で編集フォームのフローを実装する
ふとしです。
最近 flux と redux と vuex を行ったり来たりして脳みそが大変です。
redux-saga
redux-saga は redux で非同期処理を取りあつかう際の選択肢としてあげられる middleware です。
しかし以下のような処理のみを redux-saga で行う場合、redux-thunk に対しても同様ですが、導入するには大げさすぎるという意見もあるようです。
function* save (payload) { try { yield call(Registry.api.saveProfile, payload); yield put({ type: SAVE_PROFILE_SUCCEEDED }); } catch (e) { yield put({ type: SAVE_PROFILE_FAILED, payload: e.errors }); } } function* fetch () { try { const payload = yield call(Registry.api.fetchProfile); yield put({ type: FETCH_PROFILE_SUCCEEDED, payload }); } catch (e) { yield put({ type: FETCH_PROFILE_FAILED }); } }わたしも多少そのように思っていました。
フロー全体を一つの Saga として取りあつかう
機能を知ろうとドキュメントを読んでいたところ redux-saga は pull 型のアプローチであるという記述に目がとまりました。
redux-saga では redux の Action フローからターゲットとして定めた Action を取りだすことにより一連の処理を行えます。以下の例では
take
が LOGIN LOGOUT の Action が来るまで処理をブロックします。function* loginFlow() { while (true) { yield take('LOGIN') // ... perform the login logic yield take('LOGOUT') // ... perform the logout logic } }このアプローチによりフロー全体を把握しやすい形でコードにできます。ただ LOGIN などは比較的大きな粒度なので、実際の実装にどの程度の影響があるかわかりづらいと思いました。
そこで SPA で典型的に登場する編集フォームを考えてみました。今回はかんたんなプロフィール編集フォームを想定しています。
プロフィール編集フロー
プロフィール編集フォームで必要な処理をかんたんに整理しましょう。
プロフィール編集は明確な開始と終了があるフローです。フローは初期化処理・入力受付・終了処理で構成されています。
初期化処理
フロー開始時に一度だけ実行されます。
- 既存のプロフィールデータを取得します
- 取得したデータをフォームの値として設定します
入力受付
終了条件のあるループです。
- ユーザーからの値入力を受けつけます
- ユーザーからの保存要求を受けつけます
終了条件は以下のとおりです。
- 保存が成功した
- フォームから離脱した
- フローを進行できない問題がある
終了処理
フロー終了時に一度だけ実行されます。処理内容は上記ループの入力受付が終了に至った理由によりかわります。
- 保存成功していればプロフィール画面へ
- フォームから離脱した場合は何もしない
- いずれの場合でもプロフィール編集に関する state を撤去する
プロフィール編集フロー Saga
整理したフローを Saga として実装します。
- 入力受付 を
while
とtake
を用いたループとして扱います。終了条件に至った場合はwhile
を抜けます- 初期化 はそのループ以前に記述します
- 終了処理 はそのループ以後に記述します
SET_PROFILE_EDIT_STATE
のみ Reducer が処理します。export default function* profileEditFlow () { const chan = yield actionChannel([ FETCH_PROFILE, FETCH_PROFILE_SUCCEEDED, FETCH_PROFILE_FAILED, INPUT_PROFILE_VALUE, RESET_PROFILE_VALUE, SAVE_PROFILE, SAVE_PROFILE_SUCCEEDED, SAVE_PROFILE_FAILED, LEAVE_PROFILE_EDIT_FLOW, END_PROFILE_EDIT_FLOW, ]); // # 初期化処理 let reason = ''; let isLeft = false; yield put({ type: FETCH_PROFILE }); // フォームのロック処理 // 同期的に API リクエストを行っている場合、同じループに含めると take がその call 後になる yield fork(function* () { const chan = yield actionChannel([ LOCK_PROFILE_EDIT, UNLOCK_PROFILE_EDIT, CLEAN_PROFILE_EDIT_FLOW, ]); flow: while (true) { const action = yield take(chan); const state = (yield select()).profileEditState; switch (action.type) { case LOCK_PROFILE_EDIT: yield put({ type: SET_PROFILE_EDIT_STATE, payload: { ...state, isLocked: true } }); continue; case UNLOCK_PROFILE_EDIT: yield put({ type: SET_PROFILE_EDIT_STATE, payload: { ...state, isLocked: false } }); continue; case ENDED_PROFILE_EDIT_FLOW: break flow; default: continue; } } }); // # 入力受付 flow: while (true) { const action = yield take(chan); const state = (yield select()).profileEditState; switch (action.type) { case FETCH_PROFILE: yield put({ type: LOCK_PROFILE_EDIT }); yield call(fetch); continue flow; case FETCH_PROFILE_SUCCEEDED: yield put({ type: SET_PROFILE_EDIT_STATE, payload: { ...state, values: { ...action.payload }, defaultValues: { ...action.payload } } }); yield put({ type: UNLOCK_PROFILE_EDIT }); continue flow; case FETCH_PROFILE_FAILED: // 終了条件 reason = 'server error'; break flow; case INPUT_PROFILE_VALUE: yield put({ type: SET_PROFILE_EDIT_STATE, payload: { ...state, values: { ...state.values, ...action.payload } } }); continue flow; case RESET_PROFILE_VALUE: yield put({ type: SET_PROFILE_EDIT_STATE, payload: { ...state, values: { ...state.defaultValues } } }); continue flow; case SAVE_PROFILE: yield put({ type: LOCK_PROFILE_EDIT }); yield call(save, state.values); continue flow; case SAVE_PROFILE_SUCCEEDED: // 終了条件 reason = 'succeeded'; break flow; case SAVE_PROFILE_FAILED: yield put({ type: SET_PROFILE_EDIT_STATE, payload: { ...state, errors: action.payload } }); yield put({ type: UNLOCK_PROFILE_EDIT }); continue flow; case LEAVE_PROFILE_EDIT_FLOW: // 終了条件 reason = 'left'; isLeft = true; yield put({ type: END_PROFILE_EDIT_FLOW }); continue flow; case END_PROFILE_EDIT_FLOW: yield call(chan.close); break flow; default: continue flow; } } // # 終了処理 switch (reason) { case 'server error': yield put({ type: NOTIFY, payload: 'server error' }); yield call(Registry.history.push, '/@'); break; case 'succeeded': yield put({ type: NOTIFY, payload:'saved' }); isLeft || (yield call(Registry.history.push, '/@/profiles')); break; case 'left': // do nothing break; default: yield call(Registry.logger.log, '?'); yield call(Registry.history.push, '/@'); break; } yield put({ type: CLEAN_PROFILE_EDIT_FLOW }); }
take
を用いることで入力中に起こりうる出来事を一元管理できました。また初期化処理と終了処理で何が起こるかも一目瞭然です。本人以外が処理内容を把握するために断片化されたコードを手繰る困難さが軽減されたと思います。
フォームのロック処理用のループについて
非同期処理を同期的に行っている
call(fetch)
call(save, state.values)
をfork
で行うことによりロック処理も同ループに含められます。今回は保存中にページ離脱され、その後に発生した
SAVE_PROFILE_SUCCEEDED
を捉えて通知するというケースを考えたためcall
を使用しています。
call
について
call
は非同期関数を同期的に扱う (返り値が必要であればそれを得る) ために使用されますが、同期関数を呼び出すことも可能です。同期非同期に関わらず
call
を用いることでフロー内で起こる出来事として観測可能になります。そうすることによりテストを有利に進められるため、フロー内での関数呼び出しはcall
で行うことをおすすめします。Saga のテスト方法について詳細はドキュメントを参照してください。
https://redux-saga.js.org/docs/advanced/Testing.htmlプロフィール編集フローの起動と終了
プロフィール編集フローはプロフィール編集フォームに至った時点で起動されます。また、フォームから離脱した場合は終了されます。そこで今回は
didMount
willUnmount
に相当する時点でそれぞれの Action を発行します。また入力や保存に際しては、プロフィール編集フロー Saga が
take
する Action 以外は発行しないようにすることで、コードを手繰る負荷を低減できます。function ProfileEditForm () { const dispatch = useDispatch(); const { isLocked, errors, values: { name, email, message, }, } = useSelector(s => s.profileEditState); useEffect(() => { dispatch({ type: START_PROFILE_EDIT_FLOW }); return () => { dispatch({ type: LEAVE_PROFILE_EDIT_FLOW }); }; }, []); const submit = e => e.preventDefault() || dispatch({ type: SAVE_PROFILE }); const reset = () => dispatch({ type: RESET_PROFILE_VALUE }); const setter = k => e => dispatch({ type: INPUT_PROFILE_VALUE, payload: { [k]: e.target.value } }); const errorMessages = errors.length ? <>{errors.map(m => <p key={m}>{m}</p>)}</> : null; return ( <form onSubmit={submit}> <h1>Profile</h1> {errorMessages} <div> <label htmlFor="name">Name:</label> <input type="text" disabled={isLocked} id="name" value={name} onChange={setter('name')} /> </div> <div> <label htmlFor="email">Email:</label> <input type="text" disabled={isLocked} id="email" value={email} onChange={setter('email')} /> </div> <div> <label htmlFor="message">Message:</label> <input type="text" disabled={isLocked} id="message" value={message} onChange={setter('message')} /> </div> <button type="submit" disabled={isLocked}>Save</button> <button type="button" disabled={isLocked} onClick={reset}>Reset</button> </form> ); }余談: 上記のコードと実際に使っているコードのちがい
- ActionCreator を使っています
- TypeScript 使っています。Action に適切な型を与えることにより
switch
がタイプガードとして働きますまとめ
redux-saga を最初に導入した時にはこのエントリーの最初に書いてるような単純な使い方しかしていませんでした。
ドキュメントを読んだところ pull 型を実現する
take
こそが redux-saga のパワーではないのかと思い、そのように実装してみたところこのようになりました。そして好ましいと感じました。より複雑なフォーム、例えばショッピングカートではいくつかのフォームを進んでいく高レベルなフローが存在する場合があります。(ドキュメントに記載されていた LOGIN LOGOUT のフローも高レベルですね) その場合も同じように一つのフローとして考え記述することにより、全体で何が起こるのかがわかりやすくなると思います。
redux-saga の使い方のヒントになれば幸いです。
- 投稿日:2019-07-28T01:37:04+09:00
CSS modulesを使ってCSSで名前空間を実現する
CSSは、勉強してないのをごまかすためにイキって強がる人の風評被害を受けやすく、実際よりも低く評価されてしまうことがよくあります。
でも現代のCSSは意外に良いものですし、依然として残る課題に対しても「名前空間がないからCSSマジつかえねぇわ〜」と知ったかぶりして書かない理由を探すのではなく、やはり生産的に解決したいものです。そこで、この記事ではCSS modulesを使って お手軽に CSSに名前空間を擬似的に導入します。
また、ParcelでCSS modulesを使う時に遭遇するバグの回避方法もあわせてお見せします。CSSに名前空間がないことで起きる問題
CSSには名前空間がないため、たとえばこれからご紹介する例のように複数のCSSファイルを読み込んだ際に意図せぬ見た目になってしまうことがあります。
まず、以下の
yagi.css
だけを読み込んでみましょう。yagi.css.yagi { width: 10em; } .big { width: 50em; } .yagi::after { content: ''; display: block; background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg'); background-size: contain; background-repeat: no-repeat; padding-top: calc(100% * 225 / 400); width: 100%; }yagi.html<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="yagi.css"> </head> <body> <div class="yagi"></div> <div class="yagi big"></div> </body> </html>
yagi.css
に設定した.big
クラスの設定が効いて $\huge{大きなヤギ}$ の画像になっています。次に、以下の
button.css
だけを読み込んでみましょう。button.css.button { padding: 0.6em; font-size: 1em; } .big { font-size: 3em; }button.html<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="button.css"> </head> <body> <button type="button" class="button">Button</button> <button type="button" class="button big">Big Button</button> </body> </html>
button.css
に設定した.big
クラスが効いてボタンが大きくなっています。では、
yagi.css
とbutton.css
を両方読み込むとどうなるでしょうか。mix.html<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="yagi.css"> <link rel="stylesheet" href="button.css"> </head> <body> <button type="button" class="button">Button</button> <button type="button" class="button big">Big Button</button> <div class="yagi"></div> <div class="yagi big"></div> </body> </html>ぶっこわれました。
ヤギ画像にもボタンにも以下の 両方の CSSが適用されてしまったためです。
yagi.css.big { width: 50em; }button.css.big { font-size: 3em; }このように、一般的過ぎるクラス名を使うと、意図しない要素にそのCSSが影響してしまいます。
BEM記法
技術ではなく運用でこの問題を解決しようとするのがBEM記法です。
先ほどの例では、以下のようにクラス名をつけなおします。yagi-bem.css.yagi { width: 10em; } .yagi--big { width: 50em; } .yagi::after { content: ''; display: block; background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg'); background-size: contain; background-repeat: no-repeat; padding-top: calc(100% * 225 / 400); width: 100%; }button-bem.css.button { padding: 0.6em; font-size: 1em; } .button--big { font-size: 3em; }手作業でクラス名の頭に識別子をちまちまくっつける感じですね!
悪くはないんですが、識別子を付けないやつがチームにいると困ったことになります。また、いくらベラのキャラデザインを現代的な若い女の子にしても、同時期に放映されているゲゲゲの鬼太郎もネコ娘のキャラデザインが一新されており、脚本も攻めまくったおもしろい内容になっているので「まぁ鬼太郎があるし、妖怪人間の方は見なくていいか」となってしまいます。
CSS modules で解決する
そこで、人間の手にたよらずに、ヤギの手も妖怪の手も借りずに自動で名前の衝突を避けてくれる技術の1つが CSS modules です。
では、お手軽にためしてみましょう。
nodeをインストールしていない方は先にインストールしておいてください。
以下はLinux OS上での操作を想定していますが、Macでもだいたい一緒だと思います。
うぃんどうずを使っている人はまずデュアルブートでUbuntuとかを入れましょう。
css-modules-sample
みたいなディレクトリを作り、そのディレクトリの中で$ npm i postcss-cli postcss-modules
を実行して必要なライブラリをローカルインストールしましょう。
あとは、CSSファイルと以下の設定ファイルをこのディレクトリに置いておきます。(設定ファイルの詳しい仕様を知りたくなったら公式ドキュメントをご参照ください)
postcssrc.jsmodule.exports = { "modules": true, "plugins": { "postcss-modules": {}, } }これで準備は完了です。
このディレクトリ内にyagi.css
を用意して、以下のコマンドを実行してみましょう。$ npx postcss yagi.css -o dist/yagi.cssあらたに
yagi.css.json
とdist/yagi.css
が生成されているはずです。
dist/yagi.css
をひらいてみると、以下のような内容になっています。dist/yagi.css._yagi_1d9sa_1 { width: 10em; } ._big_1d9sa_5 { width: 50em; } ._yagi_1d9sa_1::after { content: ''; display: block; background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg'); background-size: contain; background-repeat: no-repeat; padding-top: calc(100% * 225 / 400); width: 100%; }なんかランダムっぽい識別子がくっついて名前の衝突を防げるようになっていますね?
yagi.css.json
は、オリジナルのクラス名がCSS modulesによってどのように変換されたかをJSON形式で保持しています。yagi.css.json{"yagi":"_yagi_1d9sa_1","big":"_big_1d9sa_5"}なんらかの工夫をして、HTML側のタグにつけるクラス名もこのJSONファイルを見て変換しないといけません。
Reactとかだと何かそういうライブラリとかがあるみたいです。
でも、ちょっとそれってめんどくさいですよね?
そこでそんなめんどくさがりの人のために、ずるいやり方 があります。設定ファイル
postcssrc.js
を以下のように書き換えてしまってください。postcssrc.jsmodule.exports = { "modules": true, "plugins": { "postcss-modules": { "generateScopedName": "[name]__[local]", "getJSON": () => null } } }postcss-modulesの説明にあるとおり、
generateScopedName
は「どのようにクラス名を変換するか」のルールを指定しgetJSON
は変換の対応表の書き出し方を指定しますここでは
generateScopedName
にファイル名__元のクラス名
という形式を指定し、getJSON
には対応表は特に生成しないように指定しています。
実行して結果を見てみましょう。$ npx postcss yagi.css -o dist/yagi.cssdist/yagi.css.yagi__yagi { width: 10em; } .yagi__big { width: 50em; } .yagi__yagi::after { content: ''; display: block; background-image: url('https://user-images.githubusercontent.com/1481749/56465716-251ebf00-643f-11e9-8c66-8d0de8953663.jpg'); background-size: contain; background-repeat: no-repeat; padding-top: calc(100% * 225 / 400); width: 100%; }これなら事前にどのようなクラス名に変換されるか予測できるため、HTML側を書く時に
mix2.html<!DOCTYPE HTML> <html> <head> <meta charset="UTF-8"> <link rel="stylesheet" href="yagi.css"> <link rel="stylesheet" href="button.css"> </head> <body> <button type="button" class="button__button">Button</button> <button type="button" class="button__button button__big">Big Button</button> <div class="yagi__yagi"></div> <div class="yagi__yagi yagi__big"></div> </body> </html>としておけばいいだけです。
Elmを使っている方は、「Elmでもっと気軽にCSS modules」で紹介した方法を使うと簡単です。
Parcelで使う
ファイルを結合したり、Babelを使ったり、Minifyしてくれたり、そういう面倒なタスクをお手軽にいろいろ勝手にやってくれる技術の1つにParcelがあります。
Parcelを使う場合は先ほどの.postcssrc.js
をディレクトリ内に置いた状態で$ npm i parcel https://github.com/arowM/parcel/archive/parcel-bundler@1.12.4.tar.gz $ parcel build mix2.html --public-url ./とすれば、
dist
にコンパイル後のファイルが生成されます。サンプルリポジトリ を作っておいたので、このリポジトリをcloneして
$ npm i && npm run buildとしても同じことができます。
この通り、うまくいきました!
なお、Parcel には postcss-modules の
generateScopedName
が効かないという問題があり、これを修正したものがまだリリースされていません。
そこで、上記のコマンドやサンプルリポジトリでは、parcelをフォークして独自に修正した parcel-bundler@1.12.4 をインストールしています。
- 投稿日:2019-07-28T00:57:07+09:00
「2019年にReact Nativeを使い始める:初めてのアプリケーションを構築する」をやってみた
今までReact Nativeをやってみたいと思っていたのですが、なかなか機会がなく、今回↓のチュートリアルがとてもわかりやすそうでしたのでやってみました。
Getting Started with React Native in 2019: Build Your First App
この投稿で書いてある内容は、素人の私が参考サイトの通りやってみただけの内容です。
参考サイトの方では詳しい解説などもあるため、私の投稿は読まずに参考サイトを見れば大丈夫です。
ですが一応やってみたので投稿させていただきました。バージョン
- macOS Mojave 10.14.5
- react 16.8.6
- react-native 0.60.4
- Xcode 10.2
- Android Studio 3.3.2
React Native CLI のインストール
$ yarn add -D react-native-cli $ yarn react-native --version yarn run v1.17.3 warning package.json: No license field $ /Users/kengookumura/dev/tmp/rn/node_modules/.bin/react-native --version react-native-cli: 2.0.1 # ←react-native-cliのバージョンを確認 react-native: n/a - not inside a React Native project directory ✨ Done in 0.19s.React Native Appを実行
yarn react-native init EmojiDictRN cd EmojiDictRN/
アプリを起動
yarn startiOSで起動
yarn start
で実行しているのとは別のターミナルのタブで↓を実行するyarn react-native run-iosiOSシミュレータを起動することができました
※最初Xcode 11.0 beta 3がインストールされている状態で行ったのですが、以下のようにエラーが出ました。
Xcode 10.2をインストールして動かせましたerror Could not find "iPhone X" simulator.↓からXcode 10.2をダウンロードしました
Androidで起動
yarn start
で実行しているのとは別のターミナルのタブで↓を実行するbashyarn react-native run-android #※私の環境ではエラーになりました
エラーになってしまったので、自分がやってみことです
まずこちらを参考にし、
ANDROID_HOME
のパスを設定しましたexport ANDROID_HOME=/Users/kengookumura/Library/Android/sdk私の環境では↑だけではうまくいかず、
↑こちらのリンクで、「アプリケーションを実行するには、Androidシミュレータを起動するか、デバイスを接続する必要がある」とあっため、↓を参考にエミュレータを起動した上で
yarn react-native run-android
を実行するとうまくいきましたApp.jsを変更してみる
参考サイトの通りに
App.js
を変更して、iOSシミュレータの場合はcommand + Rでリロードすると、App.jsimport React, { Component } from 'react'; import { Platform, StyleSheet, Text, View } from 'react-native'; export default class App extends Component { render() { return ( <View style={styles.container}> <Text style={styles.instructions}>Hello World!</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF' }, instructions: { textAlign: 'center', color: '#333333', marginBottom: 5 } });表示を変更することができました
Hot Reloadする
iOSの場合ですと、iOSエミュレータのメニューで[Hardware] > [Shake Gesture]とするとメニューが表示されるので、
「Enable Hot Reloading」を選択するとHot Reloadしてくれました簡単にコンポーネントを作成する
src/components/EmojiDict.js
ファイルを作成するsrc/components/EmojiDict.jsimport React, { Component } from 'react'; import { View, Text, StyleSheet } from 'react-native'; class EmojiDict extends Component { state = { '?': '? Smiley', '?': '? Rocket', '⚛️': '⚛️ Atom Symbol' }; render() { return ( <View style={styles.container}> <Text>{this.state['?']}</Text> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center' } }); export default EmojiDict;App.jsimport React, { Component } from 'react'; import { Platform, StyleSheet, Text, View } from 'react-native'; import EmojiDict from './src/components/EmojiDict'; export default class App extends Component { render() { return <EmojiDict />; } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF' }, instructions: { textAlign: 'center', color: '#333333', marginBottom: 5 } });
emojiのリストを
FlatList
で表示するするsrc/components/EmojiDict.jsimport React, { Component } from 'react'; import { Text, StyleSheet, FlatList } from 'react-native'; class EmojiDict extends Component { state = { '?': '? Smiley', '?': '? Rocket', '⚛️': '⚛️ Atom Symbol' }; render() { return ( <FlatList contentContainerStyle={styles.container} data={[ { key: '?', value: '? Smiley' }, { key: '?', value: '? Rocket' }, { key: '⚛️', value: '⚛️ Atom Symbol' } ]} renderItem={({ item }) => <Text>{item.value}</Text>} /> ); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center' } }); export default EmojiDict;
React Nativeについてはわからないことだらけですが、とりあえず動かすことができたとてもありがたかったです。
読んでいただいてありがとうございました。m(_ _)m