🤖

Firebase functions

2024/05/26に公開
3

関数の中身の見方

  1. Firebaseコンソールからfunctions管理画面へ
  2. 三点リーダから「使用状況の詳細な統計情報」
  3. 開いた Cloud Functions からソースタブへ
  4. [zipでダウンロード]

カスタムフック不要!! Reactとオブジェクト指向で完全state管理

Reactは意外とオブジェクト指向と相性がいいです

状態管理ライブラリが不要になります

  • 不要: Redux、Jotai、Zustand、TanStack Query
  • カスタムフックも不要になります。

用語

  • Model: User、Media、Comment、Room、ChatRoomといったエンティティです。
  • 階層型オブジェクト: users[].medias[].comments[] といったエンティティらをチェーンで繋げられるインスタンスのことです。

実装

Auth.js
import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
} from 'firebase/auth';
import { doc, setDoc } from 'firebase/firestore';
import { auth, db } from './config';
import { useState } from 'react';
import { useRouter } from 'next/navigation';

export default class Auth {
  // Rule of Hooksに則り、**constructor内でのみ**フックを使用します。
  // よって、newは トップレベル もしくは Reactの関数から のみできます。(利用側参照)
  constructor() {
    [this.state, this.setState] = useState();
    this.router = useRouter();
  }
  getStateLogined() {
    if (this.state === undefined)
      return 'unloaded';
    else if (this.state === null)
      return 'unlogined';
    else
      return 'logined';
  }
  async signUpWithEmail({ email, password }) {
    return await createUserWithEmailAndPassword(
      auth,
      email,
      password
    ).then(async (userCredential) => {
      await this.pushUser(userCredential.user, password);
      return userCredential.user;
    });
  }
  async loginWithEmail({ email, password }) {
    return await signInWithEmailAndPassword(
      auth,
      email,
      password
    );
  }
  async logout() {
    await signOut(auth);
    this.router.push('/');
  }
  // TODO: Usersクラスにおけそうである
  async pushUser(user, password) {
    const docRef = doc(db, 'users', user.uid);
    // document の追加
    return await setDoc(docRef, {
      uid: user.uid,
      email: user.email,
      password,
    });
  }
}
AppProvider.jsx
const AppProvider = ({ children }) => {
    // 再レンダーされてnewを呼ばれても、(useState同様、)内部のstateは保持されている。
    const auth = new Auth();

    // useEffectやonClickなど非同期処理内で: () => {
      auth.logout(); // 自動でホーム画面に飛んでくれます
    }
    
    return <AppContext.Provider value={{ auth, mine }}>{children}</AppContext.Provider>

stateを持たないクラスは?

逆に、"絶対にuseStateを持たない"って決めたクラスなら、
利用側は一度だけnewされるよう固定してあげる必要があります。
(再レンダーに巻き込まれて何度もnewしない為です。)

AppProvider.jsx
const AppProvider = ({ children }) => {
    const [xxx] = useState(() => new Xxxx());
    const [queryClient] = useState(() => new QueryClient())

課題と解決

非同期でfetchしてくるような子孫stateは 、もちろんuseStateが使えない (副作用内でuseStateは使えないから)。

案:
トップレベルのみstateを持ち、子孫の更新はトップレベルで行う

毎回トップレベルのsetStateを叩いて、負荷はどのくらいあるのだろう...

Discussion

Honey32Honey32

失礼します。

React には Rules of Hooks という規約があるので、このように「コンストラクタの中でフックを呼び出して、返り値を、そのオブジェクトのプロパティとして保存する」べきではありません。

https://ja.react.dev/reference/rules#rules-of-hooks

特に、「そのオブジェクトのプロパティとして保存する」ということをしてしまうことで、「State as Snapshot」と呼ばれる React の原則を無視することになり、予測できない挙動の原因になります。

言い換えると、React は「この関数はいつ呼び出され、この式はいつ評価されるのか」が大事なので、「オブジェクト指向」に覆い隠さずに、きちんと分かるように、愚直に書くべきです。(そして、愚直な書き方でカプセル化する方法こそが「カスタムフック」なのです)

https://qiita.com/honey32/items/ee8d1577e68b0d58678d


実は、React とともに使うライブラリがクラスを使用するケースもありますが、これらは「React のコンポーネントのライフサイクル」と「自作クラスのライフサイクル」をキチンと分離していることに注意が必要です。

https://github.com/TanStack/query/blob/main/packages/query-core/src/queryClient.ts

以下のように呼び出すことで、「ステートからは独立したもの」として、複雑な状態管理を隠蔽するオブジェクト(QueryCleitnt)を扱うことが可能になります。

function App() {
  const [queryClient] = useState(() => new QueryClient())
  return (
    <QueryClientProvider client={queryClient}>
      <Home />
    </QueryClientProvider>
  )
}

https://tanstack.com/query/latest/docs/eslint/stable-query-client