みなさんこんにちは。
株式会社BeatFitでCTOをしている、飯塚と申します。
私達は2021年4月から、microCMSをReact Nativeアプリに導入しました。
詳細につきましては、こちらの記事をご覧ください。
今回開発をしていく中で、いろいろな場所でハマってしまいました。
Jamstackの知見不足や、write要件を要する複雑な設計に難渋したことが主な理由ですが、そもそも困った時に参照する資料が少なかったことが辛いところでした。
ReactやVueと違い、React Native自体がそこまで広く普及していないこと、国産CMSのため海外事例がほとんど見当たらないためと考えられました。
本記事が、今後 React NativeにmicroCMSを導入しようと考えている開発者の皆さまに、少しでもお役になれば幸いです。
前提
下記のバージョンで開発を行っています。バージョンの差異によって若干機能が異なる可能性があります。
- react-native 0.63.4
- react 16.13.1
- react-dom 16.13.0
設計
私達のアプリでは、本年4月からみんなの広場という機能が導入されました。
その中では、リスナーさんから運営側へ悩みや疑問を共有したり、運営側からリスナーさんへ運動会や表彰などのイベントを告知したりと、お互いが交流する場所となってます。
リスナーさんはアプリからFeed投稿をする一方で、運営側はmicroCMSのリッチエディタ を使ってWeb上からFeedを投稿したいというニーズがありました。
運営側の投稿とリスナーさんの投稿は、同じコンテンツとしてアプリ上に表示されるため、以下が悩ましいポイントでした。
・React Nativeでリッチエディタ機能を実現するにはどうしたらよいか?
React Native の標準Componentにリッチエディタ相当の機能はないため、外部ライブラリーを探す必要がありました。
・HTMLレスポンスを、React Native上にどうやって反映させるか?
microCMSのリッチエディタで投稿した内容は、APIのGETリクエストでレスポンスをHTML形式で取得できます。
"<p>リッチエディタの投稿です。</p><p><br></p><p><br></p><p>レスポンスはHTML で取得できます。</p>",
React Nativeでは、上のHTMLを直接レンダリングできないため、外部ライブラリーを探す必要がありました。本記事では、これら2つの問題を解決する上で、とても苦労した話を中心にご紹介します。
使用したライブラリー
react-native-pell-rich-editor
「react native rich editor」 で検索すると、いくつかのライブラリーがヒットします。
npm trendsでは、react-native-pell-rich-editorが直近の半年間で最もダウンロードされてました。
Star数、メンテナンス頻度、Issue、ライブラリーのソースコードなどを確認し、実際にローカルでの動作検証を行いました。
ライブラリーのソースコードを見ると、約50種類の機能を選択できるようでした。
特に動作上の問題を認めず、microCMSとの差分を吸収できると判断し、採用する方針としました。
はまったところ
Focus 時の問題
以下は、react-native-pell-rich-editorをインストールし、何かを入力をしようとタップした時の挙動です。
私達は、この動画に見られる以下の3つの問題に対応しなければなりませんでした。
・Keyboard がテキストエディタ上に被ってしまう問題
この問題は、標準ComponentであるTextInputを使用しても同様に発生する、割と一般的な問題です。この対策にはKeyboardAwareScrollViewを使用し、具体的に以下のような実装となりました。
import React, { useRef } from 'react'
import { View, ScrollView } from 'react-native'
import { KeyboardAwareScrollView } from '@codler/react-native-keyboard-aware-scroll-view'
import { RichEditor } from 'react-native-pell-rich-editor'
const scrollRef = useRef<ScrollView | null>()
const onFocus = () => {
scrollRef.current?.scrollTo({ x: 0, y: 200, animated: true })
}
<KeyboardAwareScrollView
innerRef={ref => (scrollRef.current = (ref as unknown) as ScrollView)}
enableOnAndroid
>
<View style={styles.richEditorView}>
<RichEditor
ref={r => (richText.current = r)}
onFocus={onFocus}
/>
</View>
</KeyboardAwareScrollView>
・Keyboard 入力の最初の一文字が二重に入力されてしまう問題
キーボードでアルファベット以外を入力すると、二重に入力されます。具体的には、最初に「あ」をタップすると「ああ」と2文字入力されます。
こちらについては、2021年3月時点ですでに Issueが報告されており、defaultParagraphSeparator={""}
をpropsに設定することで、解決します。
<RichEditor
ref={r => (richText.current = r)}
onFocus={onFocus}
defaultParagraphSeparator={""}
/>
・完了ボタンがない問題
KeyboardのReturnKeyが改行になっており、またKeyboardの直上に完了ボタンが存在しないため、デフォルトではテキストの入力を終了することができませんでした。また、後述のBlur機能がうまく機能せず、追加実装が必要でした。
こちらへの対策として、react-native-keyboard-accessoryというライブラリーを使用して、完了ボタンを配置する方法を取りました。具体的には以下のような実装になります。
<KeyboardAwareScrollView
innerRef={ref => (scrollRef.current = (ref as unknown) as ScrollView)}
enableOnAndroid
>
<View style={styles.richEditorView}>
<RichEditor
ref={r => (richText.current = r)}
onFocus={onFocus}
/>
</View>
</KeyboardAwareScrollView>
<KeyboardAccessoryNavigation
doneButtonTitle="完了"
nextHidden
previousHidden
androidAdjustResize
/>
これらの設定により、最終的なFocus時の挙動は以下のようになりました。
Blur 時の問題
続いて、Keyboardを閉じる時に発生した問題の紹介となります。
通常React NativeでKeyboardを閉じる時は、Keyboard API のdismissメソッドを呼びますが、リッチエディタは react-native-webviewがベースのため、これがうまく機能しませんでした。
しかしながら、昨年12月のリリースで、onBlur propsが導入されたたため、こちらからrefを設定してblurContentEditorメソッドを呼ぶことで、閉じる動作を実装できます。先ほどの完了ボタンを押した時、及び、リッチエディタ外部をタップした時に、このメソッドを呼ぶように設定しました。
const richText = useRef<RichEditor | null>()
const onDone = () => richText.current?.blurContentEditor()
<TouchableWithoutFeedback onPress={onDone}>
<View style={styles.sectionTitleView}>
</View>
</TouchableWithoutFeedback>
<View style={styles.richEditorView}>
<RichEditor
ref={r => (richText.current = r)}
onFocus={onFocus}
onBlur={onDone}
/>
</View>
<KeyboardAccessoryNavigation
doneButtonTitle="完了"
nextHidden
previousHidden
androidAdjustResize
onDone={onDone}
/>
Submit 時の問題
リッチエディタ上で入力した内容を、HTML形式に変換してサーバーへ送信するにはどうしたら良いでしょうか?
従来は、refを設定して getContentHtml
メソッドを使ってHTMLを取得していたのですが、こちらが deprecated APIになり、代わりにonChange
メソッドを使うと良いとのことでした。(詳細は、ソースコードをご覧ください。)
実装は以下のようになります。
const [body, onChangeBody] = useState('')
const handleChange = (text: string) => onChangeBody(text)
<RichEditor
ref={r => (richText.current = r)}
onChange={handleChange}
onFocus={onFocus}
onBlur={onDone}
/>
これで、リッチエディタから入力したテキストが、HTML形式でstateのbodyに格納されるので、この値をサーバーへ飛ばせば良さそうです。
まとめ
react-native-pell-rich-editorは高機能なライブラリーで、microCMSのリッチエディタ相当の機能を持ちます。
しかしながら、Focus時、Blur時、Submit時にそれぞれ配慮する点があり、2021年5月の時点では既述の方法で対応できます。今後はより良い解決策が出るかもしれませんので、最新のライブラリの開発状況を追って、適宜対応してください。
[参考文献]
Issue: keyboard.dismiss() not work
Issue: How to add Keyboard.dismiss on IOS
StackOverflow: Hide keyboard on enter/next press with react-native-pell-rich-editor
Issue: How to get content we typed dynamically to the editor ?
react-native-render-html
次は、リッチエディタから生まれたHTMLを、React Native上で反映させるために必要なライブラリーの話に移ります。
「react native html trends」 で検索したところ、3つのライブラリーがヒットしました。
npm trendsでは、react-native-render-htmlが直近の半年間で最もダウンロードされており、弊社もこちらを採用しました。
react-native-render-htmlは、HTMLをWebViewを介さずに、100%ネイティブビューにレンダリングするライブラリーで、実装は以下のようになります。
import React, { Component } from "react";
import { ScrollView, useWindowDimensions } from "react-native";
import HTML from "react-native-render-html";
const htmlContent = `
<h1>This HTML snippet is now rendered with native components !</h1>
<h2>Enjoy a webview-free and blazing fast application</h2>
<img src="https://i.imgur.com/dHLmxfO.jpg?2" />
<em style="textAlign: center;">Look at how happy this native cat is</em>
`;
function Demo() {
const contentWidth = useWindowDimensions().width;
return (
<ScrollView style={{ flex: 1 }}>
<HTML
source={{
html: htmlContent
}}
tagsStyles={{
span: { fontSize: 15 },
p: { color: '#787878' },
div: { color: '#787878' },
}}
contentWidth={contentWidth}
/>
</ScrollView>
);
}
source propsに、サーバーから取得したHTMLを当てはめるだけで、React Native上でレンダリングできます。また上の例のように、tagStylesの設定で、タグに個別のスタイルを当てはめることができます。
はまったところ
CSSの問題
文章の先頭行に、正しくCSS が設定されない問題が発生しました。
ご覧の通り、「こんにちは!」のテキストに、CSSがうまく反映されておりません。
原因ですが、react-native-pell-rich-editorのFocus時の対応で、defaultParagraphSeparator={""}
を設定すると、先頭行にタグが反映されなくなることが原因でした。
取得されるHTML:
"テスト<div><br></div><div>CSSが一行目に</div><div>何故か適応されないです。</div>"
ご覧の通り、先頭の行「テスト」の前後にタグがついておりません。
こちらへの対策として
<HTML
source={{
html: `<div>${htmlContent}</div>`,
}}
tagsStyles={{
span: { fontSize: 15 },
p: { color: '#787878' },
div: { color: '#787878' },
}}
contentWidth={contentWidth}
/>
と全体をdivタグで囲み、tagStylesで設定したCSSが全体に反映される修正を入れました。
しかしながら問題の根本解決策ではないため、今後のライブラリの状況を追っていきたいと思います。
おわりに
今回は、React Native上でリッチエディタを実装する機能について解説しました。
通常、HeadlessCMSを導入する上で、今回のようにフロントエンド側にもリッチエディタを導入するケースは少ないかと思います。
一方で、コンテンツを投稿する際に、運営側はmicroCMS経由で、ユーザー側はアプリ経由で、と別々の設計になった場合に、この記事の内容がお役に立てば嬉しいです。