Open27

React Native まとめ

jintzjintz

結論として、処理速度の観点から値の算出はすべてselectorを利用した方がよく、Recoil依存性の観点からカスタムフックでselectorをラップした方がいいと考える。
ソースは特にない。多分こうでしょレベルだから、何か違った意見があれば随時考え直すべき。

https://zenn.dev/jintz/scraps/e9355bb1158408

jintzjintz

React Native Firebaseのauth().onStateChenged()とRecoilのAtom Effectsの噛み合わせが悪い。
Atom EffectsでonStateChengedをサブスクライブするとログアウト時にユーザ情報が残ったまま更新されてしまうため、ユーザ情報が消えない。
普通にuseEffectでやる分には問題ないんだけど、Atom Effectsを噛ませると普通にuseEffectでやってる方まで変になる。
Atom Effectsは (というかRecoilもだけど) まだまだExperimentalらしいから、その辺がうまく解消されていないのかな。あるいは他にうまいやり方があるのか。

jintzjintz

結論として、Atom EffectではなくRecoilないしRecoilStateとの相性が悪いみたい。

const login = () => {
  const doLogin = async () => {
    try {
      await GoogleSignin.hasPlayServices();
      const { idToken } = await GoogleSignin.signIn();
      const credential = auth.GoogleAuthProvider.credential(idToken);
      await auth().signInWithCredential(credential);
    } catch (error: unknown) {
      // エラー処理
    }
  };

  doLogin();
};

const logout = () => {
  const doLogout = async () => {
    await auth().signOut();
    await GoogleSignin.signOut();
  };

  doLogout();
};

↑のような関数を定義する。

const [user, setUser] = useState<FirebaseAuthTypes.User | undefined>(undefined);

useEffect(() => auth().onAuthStateChanged(newUser => setUser(newUser ? newUser : undefined)), []);

↑は問題ない。↓はログアウト時に正常に動作しない。

const userState = atom<FirebaseAuthTypes.User | undefined>({
  key: 'user',
  default: undefined,
});

const [user, setUser] = useRecoilState(userState);

useEffect(() => auth().onAuthStateChanged(newUser => setUser(newUser ? newUser : undefined)), [setUser]);

どういうことなの……

jintzjintz

Android版について、デフォルトのパッケージ名だとすでに利用されていることが多い。
そのため、react-native-renameでパッケージ名を変更するとよい。

$ npx react-native-rename "New App Name" -b com.yourpackage.newapp

アプリ開発を進めてから利用しようとするとエラーを吐くことがあるので、可能であればプロジェクトを作成した直後に走らせるとよい。

jintzjintz

最近はhuskyとlint-stagedの記事も見かけないけど、みんな手動でやっているのか基本すぎて誰も何も言わないだけなのか。

$ npm install -D husky lint-staged
package.json
{
  ...,
  "scripts": {
    ...,
    "prepare": "husky install"
  },
  ...,
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": "npm run lint"
  }
}
$ npm run prepare
$ npx husky add .husky/pre-commit "npx lint-staged"
jintzjintz

基本的に、理由がなければ要素を内包する全てのコンポーネントに style={{ flex: 1 }} と設定した方がいい。
特に各ページの親コンポーネントはこれだけ設定することも多いから再利用できるように命名しているけど、これっぽっちのためにわざわざ独自コンポーネントを定義するのはどうなのか。
かえって面倒になってない? という疑問。

jintzjintz

React NavigationをTypeScript環境で利用する場合、内包するコンポーネントはReact Navigation独自の型に当てはめなきゃいけないんだよね。
これが面倒。仮にReact Navigationを外す場合も上記の型定義を修正しないといけない。
いっそのことクッション的な中間型を作成してコンポジションしてしまおうか。

jintzjintz
type RootStackParamList = {
  Hoge: undefined;
  Fuga: undefined;
}

const Hoge = ({ openFuga }: { openFuga: () => void }) => (
  <Button onPress={openFuga}>press me!</Button>
)

const HogeStackScreen = ({ navigation }: NativeStackScreenProps<RootStackParamList, 'Hoge'>) => {
  return <Hoge openFuga={() => navigation.navigate('Fuga')} />
}

これでよし。

jintzjintz

基本的に、理由がなければ要素を内包する全てのコンポーネントに style={{ flex: 1 }} と設定した方がいい。
特に各ページの親コンポーネントはこれだけ設定することも多いから再利用できるように命名しているけど、これっぽっちのためにわざわざ独自コンポーネントを定義するのはどうなのか。
かえって面倒になってない? という疑問。

面倒でした。素直に全部のコンポーネントに style={{ flex:1 }} を定義していけ。

jintzjintz

https://www.reactnative.guide/8-styling/8.3-separating-styles-from-component.html

「スタイルは別ファイルに定義するべき。そうすればコンポーネントのコードは若干きれいになり、必要に応じてIOSとAndroidでのスタイルを別個に定義できる」といった内容。
逆に考えると、たいして汚くならないような小さいコンポーネントやIOSとAndroidの差分を意識する必要がないコンポーネントではいちいちファイルを分ける必要はなさそう?

コンポーネントごとにその辺の書き方を変えるとメンテナンス性が落ちそうだから、なにかしらの基準を設けたいところ。
基本は全部まとめて定義しちゃっていい気もする。

jintzjintz
export const withView = <P,>(Component: React.FC<P>, props: P) => {
  return (
    <View style={{ flex: 1 }}>
      <Component {...props} />
    </View>
}

HOCだとこういうやり方もできるっぽい。
個人的にはわりと好きだけどどうなんだろうな。今はhooksでだいたい事足りるからHOCは下火らしいし。

jintzjintz

ParametersReturnTypeみたいなユーティリティ型は使い倒した方がいい。
上位の変更が伝播するため管理箇所が減るし修正が簡単かつ堅牢になる。

jintzjintz

useStateを親に持たせて子コンポーネントで参照したい場合、Recoilを利用するか下記のように書く方が簡潔になる。

// ReturnTypeでuseStateを参照した際のundefined混入対策
type UseStateReturnType<T> = ReturnType<typeof useState<T>> extends [
  infer S | undefined,
  Dispatch<SetStateAction<infer S | undefined>>,
]
  ? [S, Dispatch<SetStateAction<S>>]
  : never;

const Parent = () => {
  const countState = useState(0);

  return (
    <View>
      <Child state={countState} />
    </View>
  );
};

const Child = ({ state: [count, setCount] }: { state: UseStateReturnType<number> }) => {
  return (
    <View>
      <Text>{count}</Text>
      <Button title="+1" onPress={() => setCount(count + 1)} />
    </View>
  );
};
jintzjintz

ts-patternでオブジェクトや配列の要素について型チェックを行うとき、異なるブランチの引数として与えられる型は保証されずチェック前の型のままとして与えられる。

type Hoge = {
  fuga: string | number;
}

const hoge: Hoge = {
  fuga: 'fuga',
}

match(hoge)
  .with({ fuga: P.string }, ({ fuga }) => console.log(`ここで${fuga}は string 扱い`))
  .otherwise(({ fuga }) => console.log(`ここで${fuga}は string | number 扱い`));

switch文のノリで扱うと上記処理で違和感を覚えることになる。
この問題については下記に記載がある。

https://github.com/gvergnaud/ts-pattern/issues/145

要するに、パフォーマンスの観点から上記には特に修正を行う予定はなく、指定した型で扱いたいなら with で処理を行えばよいとのこと。
otherwise で処理を行う場合は型指定の必要がない場合のみとし、基本は withexhaustive で処理を行うようにする方が良さそう。

jintzjintz

複数の条件で同じ処理を行いたい場合はちょっと面倒になるんだよね……
素直に毎回同じ処理を書くか別の関数に切り出して使うしかなさそう。