【react-native】参考にできないAndroidのイケてないrender制御


主旨

Android限定、レンダリングが正常に動かない現象のパワーソリューションです。
それぞれの原因は仮説ベースになっていますのでご了承ください。

備忘録と、誰かの救済策になればと思って共有します。
もし、具体的な原因、もしくはスマートな解決方法をお持ちの方は(特にカルーセル)
教えていただければ大変うれしいです!

カルーセルのレンダリングが失敗する

react-native-snap-carousel はカルーセルコンポーネントを実現するためのライブラリです。

このコンポーネントで次のようなカルーセルを作ってみます。

スワイプと、ボタンでコンポーネントの表示切り替えができるものです。

const Container = () => {
    const [activeIndex, changeSlide] = React.useState(firstItem)
    const carouselRef = React.useRef(null)

    const onNext = React.useCallback(() => {
        const lastIndex = data.length - 1
        const nextIndex = lastIndex ? activeIndex : activeIndex + 1;
        const carousel = carouselRef.current

        // ここでカルーセルに指定した値までスナップさせる
        carousel.snapToItem(activeIndex + 1)

        changeSlide(nextIndex)
    }, [activeIndex])

    return (
        <View>
            <Carousel
                ...
                ref={carouselRef}
                data={data}
                onSnapToItem={changeSlide}
                renderItem={({item}) => <Card item={item} index={activeIndex} onNext={onNext} />}
            />
            <Text style={{color: "red"}}>{`Active index: ${activeIndex}`}</Text>
        </View>
    )
}

スワイプでの表示には問題がないのですが・・・。

「次へ」ボタンを押すとAndroidではこんな表示になってしまいます。

「次へ」ボタンを押したときでも、スワイプと同じように次のカードがセンターに動いてほしいのですが、うまくいきません。

この事象でわかっていること
  • onPressしたときにonNextは動く
  • renderは発火するしstateも変化する
  • iOSでは問題ない。Androidのみ発生する

Webview × Cookie で認証が安定しない

Webview時、CookieでAuth認証を行う際にWeb側がCookieを認識しないことがありました。
使用したライブラリはreact-native-cookiesです。

スクショはないですが次のような実装を行っています。(いくつか省略しています)

// cookie.js
import CookieManager from 'react-native-cookies';

...

export const cookieSet = async (name: string, value: string) => {
  const setValue = `${name}=${value}; path=/; expires=${expiration || ''}; secure`;
  // AndroidではsetFromResponseを使用する
  await CookieManager.setFromResponse(domain, setValue);
};
// AuthWebview.jsx
import { WebView } from 'react-native-webview';

class AuthWebview extends Component {
...
  async componentDidMount() {
    const { token } = this.props;
      if (token) {
        await cookieSet('_token', token);
      }
  }

...
    return (
      <WebView
        {...this.props}
        ...
        useWebKit
        javaScriptEnabled
        thirdPartyCookiesEnabled
        sharedCookiesEnabled
      />
    );
}
この事象でわかっていること
  • 認証できるときもあれば、できないときもあるような再現性のない状態
    • 1/5くらいで発生した。
  • Promiseをawaitしていないとに似ていて、その方向でデバッグするが空振り
  • iOSでは問題ない。Androidのみ発生する

共通してどうしたか

カルーセル

setTimeoutを使用して、snapToItemの処理を遅らせます。

- carousel.snapToItem(activeIndex + 1)
+ setTimeout(() => carousel.snapToItem(nextIndex), 10)

(PromiseにしてsnapToItem直前にawait sleep(10)としてもよいです)

すると、うまく表示されるようになります

snap-carouselのissueにもこの解決策は掲載されています。

Webview

すんごく気持ちわるいのですが、stateにrenderフラグを追加し、cookieセット後にrenderが実行されるように調整します。

// AuthWebview.jsx
...
  async componentDidMount() {
    const { token } = this.props;
      if (token) {
        await cookieSet('_token', token);
      }
    setTimeout(() => {
      // Androidのためにレンダリングを遅延させる制御
      this.setState({ isRender: true });
    });
  }
...
  render() {
    if (!this.state.isRender) return null;
  ...
}

すると、認証が100%成功するようになりました。

それぞれの仮説

カルーセル

  • onNext内のchangeSlidecarousel.snapToItemが同時に実行されることが原因?
  • つまり「stateの変更によるレンダリング」と、「snapToItemによるアニメーションの実行(レンダリング)」が共存することができず、snapToItemの処理が死んでしまう?
  • この2つの処理の実行タイミングを無理やりズラしてあげることでそれぞれのレンダリングが邪魔されずに実行された?

WebviewCookie

  • CookieのセットはWebviewコンポーネントに対してstateやpropsの変更を加えないのでそもそも再レンダリングがされない
  • そのためcomponentDidMountでsetCookieしてもその結果がrenderに反映されるかどうかは不確定なまま
  • cookieがセットされた状態でrenderを走らせる状況を無理やり作ることで、「Cookieが保存された状態でWebviewがサイトにアクセスする」状況が安定した

まとめ

これまでsetTimeoutを使って問題を解決する発想がなく、結構な時間を消費しました。。。
ここで掲載した2つの事象は、状況も要因もそれぞれ異なるものですが、解決方法としてどちらもズラすためにsetTimeoutを使ったソリューションです。

WebviewとCookieに関してはcomponentDidMountではないタイミングで処理したほうがいいかもしれませんが、どこで実行するのがよいのでしょうね。

具体的な原因、スマート解決方法をお持ちの方はぜひコメントください!