複数画面で認証済みのユーザー情報を引き回したいなあと思ってどうやったらいいか調べたところ、画面間をこえてグローバルに値を保持することが出来る、React標準のContext APIというものがあることが分かったので試してみる。

このContext APIそんなに使い勝手は良くないみたいで、reduxとかrecoilでこの辺面倒見てくれるとかって噂は聞いているが、まあ標準APIで実現出来るならそれもそれで良いかな。

とりあえず画面遷移を実装

以前の記事で画面遷移出来るところまで実装したので、これをベースに進める。

Expo + ReactNative + Firestoreでラーメン一覧から詳細画面への画面遷移を実装する | もふもふ技術部

react-navigation関係の必要なライブラリをインストールする。

$ expo install react-navigation react-navigation-stack react-navigation-tabs react-native-gesture-handler react-native-reanimated react-native-safe-area-context @react-native-community/masked-view

画面遷移できた。

Simulator Screen Shot - iPhone SE (2nd generation) - 2020-10-22 at 11.59.21

Simulator Screen Shot - iPhone SE (2nd generation) - 2020-10-22 at 11.59.23

Contextを導入する

この辺を参考にした。

React Context API と useContext() の使い方 | gotohayato.com

公式チュートリアルでは1ファイルに記述するサンプルが載っていて、実際の使い方とはかけ離れていてわかりづらい。たぶんContextは外部に切り出さないとと複数箇所で呼び出せないと思う。

以下コード、tsやらjsやらが混ざったままになっててすんません。動作することを確認する目的だったので手を抜きました。

App.tsx

import React, { useEffect, useState } from "react"
import { StyleSheet } from 'react-native';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import Page1Screen from './screens/Page1';
import Page2Screen from './screens/Page2';
import { UserContext, User } from './screens/UserContext'

const MainStack = createStackNavigator(
  {
    Page1: Page1Screen,
    Page2: Page2Screen,
  }
)

const AppContainer = createAppContainer(MainStack)

export default function App() {
  const [user, setUser] = useState({} as User);

  useEffect(() => {
    // 非同期のサインイン処理をsetTimeoutで模倣
    setTimeout(() => {
      setUser({ uid: '1233455', name: 'Taro Yamada' } as User)
    }, 2000)
  }, [])

  return (
    <UserContext.Provider value={ user }>
      <AppContainer />
    </UserContext.Provider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

screens/Page1.js

import React, { useContext } from 'react';
import {
  Text,View,Button
} from 'react-native';
import { UserContext } from './UserContext'

export default function Page1Screen(props) {
    const user = useContext(UserContext)

    return (
      <View>
        <Text>Page2</Text>
        <Text>user id: {user.uid}</Text>
        <Text>user name: {user.name}</Text>
        <Button
          title="go to Page1"
          onPress={() => {
            props.navigation.navigate('Page1')
          }}
        />
      </View>
    )
}

screens/Page2.js

import React, { useContext } from 'react';
import {
  Text,View,Button
} from 'react-native';
import { UserContext } from './UserContext'

export default function Page1Screen(props) {
    const user = useContext(UserContext)

    return (
      <View>
        <Text>Page2</Text>
        <Text>user id: {user.uid}</Text>
        <Text>user name: {user.name}</Text>
        <Button
          title="go to Page1"
          onPress={() => {
            props.navigation.navigate('Page1')
          }}
        />
      </View>
    )
}

screens/UserContext.ts

import { createContext } from "react"

export interface User {
    uid: string
    name: string
}

export const UserContext = createContext({} as User)

サインイン(を模倣した)処理後にContextにユーザー情報を保持して、Page1とPage2で、ユーザーのIDと名前を表示するサンプル。

Simulator Screen Shot - iPhone 11 - 2020-10-22 at 15.20.02

Simulator Screen Shot - iPhone 11 - 2020-10-22 at 15.20.04

Contextを更新する

子コンポーネントで親コンポーネントから受け取ったContextを更新するにはどうすればいいのだろうか?

実際にこういうケース発生しそうなので調べてみたところ、ContextにContext内のユーザー情報を更新する関数(setUser())を含めてしまおうっていうアプローチがあるらしいので、その方式でやってみる。

App.tsx

import React, { useEffect, useState } from "react"
import { StyleSheet } from 'react-native';
import { createAppContainer } from 'react-navigation';
import { createStackNavigator } from 'react-navigation-stack';
import Page1Screen from './screens/Page1';
import Page2Screen from './screens/Page2';
import { UserContext, User } from './screens/UserContext'

const MainStack = createStackNavigator(
  {
    Page1: Page1Screen,
    Page2: Page2Screen,
  }
)

const AppContainer = createAppContainer(MainStack)

export default function App() {
  const [user, setUser] = useState({} as User);

  useEffect(() => {
    // 非同期のサインイン処理をsetTimeoutで模倣
    setTimeout(() => {
      setUser({ uid: '1233455', name: 'Taro Yamada' } as User)
      console.log('signedIn!')
    }, 2000)
  }, [])

  return (
    <UserContext.Provider value={{ user: user, setUser: setUser }}>
      <AppContainer />
    </UserContext.Provider>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});

screens/Page1.js

import React, { useContext } from 'react';
import {
  Text,View,Button
} from 'react-native';
import { UserContext } from './UserContext'

export default function Page1Screen(props) {
    const context = useContext(UserContext)

    return (
        <View>
            <Text>Page1</Text>
            <Text>user id: {context.user.uid}</Text>
            <Text>user name: {context.user.name}</Text>
            <Button
                title="go to Page2"
                onPress={() => {
                    props.navigation.navigate('Page2')
                }}
            />
        </View>
    )
}

screens/Page2.js

import React, { useContext } from 'react';
import {
  Text,View,Button
} from 'react-native';
import { UserContext } from './UserContext'

export default function Page1Screen(props) {
    const context = useContext(UserContext)

    return (
      <View>
        <Text>Page2</Text>
        <Text>user id: {context.user.uid}</Text>
        <Text>user name: {context.user.name}</Text>
        <Button
          title="go to Page1"
          onPress={() => {
            props.navigation.navigate('Page1')
          }}
        />
        <Button
          title="chage user value"
          onPress={() => {
            // 別のユーザー情報でuserを更新する
            const user = { uid: '9999999', name: 'Jiro Saito' }
            context.setUser(user)
          }}
        />
      </View>
    )
}

screens/UserContext.ts

import { createContext } from "react"

export interface User {
    uid: string
    name: string
}

export const UserContext = createContext({})

こんな感じで、Page2でユーザー情報を更新すると自動的にPage1の情報も更新される。ふむ、やりたいことは大体これで出来た気がする。一方でちょっとContext APIはなんとでも書けてしまう危うさを感じるっちゃ感じる。そのうち他のライブラリも試してみよう。

Untitled