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

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

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

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

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

このコンポーネントで次のようなカルーセルを作ってみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/ccc0de7b-92d4-eb4a-69ec-d7f5d4ccbb5f.jpeg" width="300"> 

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

```tsx
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>
    )
}
```

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/7f6b6a48-c967-30c8-ebc6-ede080fcdf9a.gif" width="300"> 


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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/e543c314-4fa2-066f-d8cd-a0d866799842.gif" width="300">

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

###### この事象でわかっていること

- `onPress`したときに`onNext`は動く
- renderは発火するしstateも変化する
- iOSでは問題ない。Androidのみ発生する


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

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

```ts
// 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);
};
```

```tsx
// 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`の処理を遅らせます。

```diff
- carousel.snapToItem(activeIndex + 1)
+ setTimeout(() => carousel.snapToItem(nextIndex), 10)
```
> （Promiseにして`snapToItem`直前に`await sleep(10)`としてもよいです）

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

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/49120851-3290-7f50-647e-3822e89c56ca.gif" width="300">

snap-carouselのissueにもこの解決策は掲載されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103475/bb3fb4be-1523-cfbc-18f8-e5a0abdd1137.png)

https://github.com/archriss/react-native-snap-carousel/issues/552#issuecomment-508335398

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

```jsx
// 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内の`changeSlide`と`carousel.snapToItem`が同時に実行されることが原因？
- つまり「stateの変更によるレンダリング」と、「snapToItemによるアニメーションの実行（レンダリング）」が共存することができず、snapToItemの処理が死んでしまう？
- この2つの処理の実行タイミングを無理やりズラしてあげることでそれぞれのレンダリングが邪魔されずに実行された？

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

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


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


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

---
https://github.com/nitaking/BadResolutionAndroidApp