もう複雑なState管理や非同期処理に悩まない。そう、Trelaならね

公開:2020/09/27
更新:2020/09/29
30 min読了の目安(約27400字TECH技術記事
Likes11

trela icon

この記事について

この記事は、筆者が現在製作しているフレームワークについて解説した記事です。

紹介するフレームワークはJavaScriptで使う事が出来ますが、TypeScriptを使う事で最大のメリットを受けれるフレームワークであるため、この記事で紹介するソースコードはTypeScriptを用いて解説します。そのため、JavaScriptの知識に加えてTypeScriptの理解もあるとより理解できます。

この記事で紹介するフレームワークは、まだ開発中です( 2020/09時点 )

概要について知ることが出来る。そう、Trelaならね。

Trelaは、現在開発している非同期処理管理フレームワークです。
非同期処理をシンプルにする 」ことを目標に開発しており、非同期処理をより便利に扱えるようなAPIを提供するとともに、新しいパラダイムへの緩衝材として機能するように設計しています。

※ 現在はReact Hooksに依存しているため、React.js専用フレームワークとなっていますが、Trelaが持つ概念自体は他のフレームワークでも適用できると思いますので、後々は他のフレームワークに展開したいと思っています。

※この記事ではReact.jsのソースコードとともに説明したいと思います。

インストールとリポジトリ

まだ開発途中ではありますが、ある程度の機能は実装してnpmjs.comで配信していますので、以下の方法でインストールして使うことが出来ます。

$> npm install --save trela

または、yarnでインストール

$> yarn add trela

リポジトリ

OSSとしての開発を目指していますので、以下のURLから実際のソースコードを見ることが出来ます。

※ 開発途中なので、まだ粗削りな所があったりします。

Trelaの特徴

Trelaには以下の特徴があります。

  • 記述が少なく、シンプルで分かりやすい( 宣言的・同期的に書ける )
  • 複雑な非同期処理もシンプルに書ける
  • コンポーネントの描画をシンプルに最適化する
  • Store管理もシンプルに出来る
  • TypeScriptフレンドリー

今はまだ上記の特徴について理解できないと思いますが、この記事を読んでいくと上記の意味が分かるようになると思います。

また、「 TypeScriptフレンドリー 」はTrelaの恩恵を最大限に受けるための特徴です。
この特徴はTypeScriptの特性を生かした書き方をすることによって、素晴らしい開発者体験をあなたに与えることが出来ます。私はその書き方を「 TS Hack 」と勝手に呼んでいますが、この記事ではこの「 TS Hack 」についても解説します。

※ 注意として「 TS Hack 」はあくまで開発者体験を上げるだけなので、処理的なメリットはありません。あくまでも、書き方のテクニックの一つです。

なぜ非同期処理?

多くの人にとって状態管理が必要な場面とは、通信などの非同期処理が発生する場合です。

同期的な処理では、状態管理をするほどの複雑なフラグ管理なんてほとんどしませんし、同期的な処理はReactのContext APIなどのようなAPIで事足ります。

そのため、これからの状態管理フレームワークに必要なのは、「 非同期処理によって発生する状態を管理することであり、非同期処理をシンプルにすることである 」と考え、Trelaを非同期処理に特化したモノにしました。

また、ただ通信結果をキャッシュするだけでは、今後のフロントエンドで発生する問題は解決できないだろうと考え、描画の最適化Store管理のアプローチなどもTrelaでは実装しています。

非同期処理をシンプルに書ける。そう、Trelaならね。

Trelaは「 非同期処理をシンプルにする 」事を目標に開発しているため、非同期処理をとてもシンプルかつ分かりやすく記述することが出来ます。

それを理解するために、「 Trelaを使ってない場合 」と「 Trelaを使っている場合 」を比較しながら見ていきましょう。

まずは、「 Trelaを使ってない場合 」の例からです。

Trelaを使ってない場合

import React, { useState, useEffect } from "react";

interface User {
  id: number;
  name: string;
  icon: string;
}

const fetchUser = async (uid: number) => {
  const ref = await fetch("https://example.com/api/user/" + uid);
  const json = await ref.json();

  return json as User;
};

/**
 * @description User情報をロードして、カードとして表示するコンポーネント
 */
const UserCard = ({ uid }) => {
  const [isLoading, setLoadingStatus] = useState(false);
  const [user, setUser] = useState<User>({
    id: uid,
    name: "",
    icon: "default icon path",
  });

  useEffect(() => {
    setLoadingStatus(true); // ローディング開始

    fetchUser(uid).then((result:User) => {
      setUser(result); // 通信結果をViewに反映
      setLoadingStatus(false); // ローディング終了
    });
  }, []);

  if (isLoading) return <div className="loading-bar">通信中です</div>;

  return (
    <div className="user-card">
      <div className="user-icon">
        <img src={user.icon} alt="user icon" />
      </div>

      <div className="user-name">{user.name}</div>
    </div>
  );
};

/* -- 以下、UserCardとProviderコンポーネントを使ったソースコード -- */

上記のソースコードでもシンプルですが、以下の問題点が挙げられます。

上記のソースコードの問題点

  • コールバックを使っていて、処理が見にくい( 同期的でない )
  • useState Hooksを使って、自分でState管理する必要がある( 宣言的でない )
  • useEffect Hooksの罠にハマりやすい( 値の参照など )
  • コンポーネントがマウントされるたびに、非同期処理が実行される
    • キャッシュを設けることで回避可能
  • 安全にアンマウント( コンポーネントの削除 )が出来ない
    • 非同期処理が終わる前にアンマウントした場合、非同期処理が完了後setStateしてしまう

またfetchUser関数は、Promiseを使っているのでasync/awaitを使いたいところですが、useEffect Hooksでやるには少々面倒くさいやり方をする必要があります。

// useEffectの部分だけ抜粋

useEffect(() => {
  (async () => {
    setLoadingStatus(true); // ローディング開始
    
    const result:User = awiat fetchUser(uid);

    setUser(result); // 通信結果をViewに反映
    setLoadingStatus(false); // ローディング終了
  })();
}, []);

上記のようにasync/awaitを使う事で、同期的に書けるようにはなりましたが、あまり見やすいとは言えませんね。
これを踏まえた上で、「 Trelaを使った場合 」を見ていきましょう。

Trelaを使った場合

import React from "react";
import { createTrelaContext } from "trela";

interface User {
  id: number;
  name: string;
  icon: string;
}

const fetchUser = async (uid: number) => {
  const ref = await fetch("https://example.com/api/user/" + uid);
  const json = await ref.json();

  return json as User;
};

// ProvierコンポーネントとuseTrela関数を取得
const { Provider, useTrela } = createTrelaContext({ apis: { fetchUser } });

/**
 * @description User情報をロードして、カードとして表示するコンポーネント
 */
const UserCard = ({ uid }) => {
  const { apis } = useTrela();

  // 非同期処理のRefを取得( まだ非同期処理はやってない )
  // default関数は初期値を設定する関数です
  const ref = apis.fetchUser(uid).default({
    id: uid,
    name: "",
    icon: "default icon path",    
  })

  const [user, isLoading] = ref.read(); // ここで非同期処理を実行

  if (isLoading) return <div className="loading-bar">通信中です</div>;

  return (
    <div className="user-card">
      <div className="user-icon">
        <img src={user.icon} alt="user icon" />
      </div>

      <div className="user-name">{user.name}</div>
    </div>
  );
};

/* -- 以下、UserCardとProviderコンポーネントを使ったソースコード -- */

上記が、Trelaを使ったソースコードになります。

ここで注目すべき点は、fetchUser関数を実行するのにコールバックやPromiseと言った要素が出てきていません。そのため、ソースコードを宣言的・同期的に書けています。

これはuseEffect Hooksで実装した時よりも、明らかに分かりやすく・シンプルです。

また、useState関数などのReact Hooksを使っていません。それもそのはずです。
非同期処理に伴う状態管理もTrelaが勝手にやってくれているからです。これにより、わざわざuseState関数を使って自前でState管理しなくてもよくなるので、安全にアンマウントすることが出来ます。

勿論、useEffect Hooksも使っていないので、実行タイミングの罠にはまる事もありません。

Trelaを使えば、コールバック地獄やasync/await地獄に悩まされることなく、非同期処理の結果や処理状態を受け取ることが出来ます。

もう複雑な非同期処理に悩まない。そう、Trelaならね。

Trelaを使えば、非同期処理をシンプルに書ける事は分かりましたが、非同期処理には厄介なモノがあります。

それは、直列処理並列処理です。

これら2つはasync/awaitを使えばシンプルに書けますが、描画処理が絡むコンポーネント内ではasync/awaitで書くことが難しく、また書けたとしても複数のState管理に悩むことは想像に難くありません。

Trelaでは、勿論それらの問題を解決できます。例を見ていきましょう。

直列処理並列処理の例では、全てのコードを書くとすごく長くなってしまうため、重要な部分のみ記述します。ご了承ください。

直列処理 : Trelaを使ってない場合

const UserCard = ({ uid }) => {
  const [isLoading, setLoadingStatus] = useState(false);
  const [resultData, setResult] = useState({
    isLogin: false,
    user: defaultUserValue
  });

  useEffect(() => {
  
    (async () => {
      setLoadingStatus(true); // ローディング開始
      
      // fetchLoginStatusとfetchUserを直列処理している
      const isLogin = await fetchLoginStatus(uid); 
      const user = await fetchUser(uid);
      
      setResult({ isLogin, user }); // 通信結果をViewに反映
      setLoadingStatus(false); // ローディング終了
    })();
  
  }, [])

  /* -- 以下、描画処理 -- */
}

上記の例では、fetchLoginStatus関数fetchUser関数の二つを直列に実行しています。
useEffect Hooks内でasync/awaitを使ってシンプルにしていますが、コールバックとかの影響で見にくいですし、前項で指摘した問題点も解決できていません。

それを踏まえて、次にTrelaを使った場合を見てみましょう。

直列処理 : Trelaを使った場合

const UserCard = ({ uid }) => {
  const { apis, steps } = useTrela();
  
  // 非同期処理のRefを取得( まだ非同期処理はやってない )
  const loginRef = apis.fetchLoginStatus().default(false);
  const userRef = apis.fetchUser().default(defaultUserValue);

  // fetchLoginStatus -> fetchUserの順に直列実行するRefを取得
  const stepsRef = steps([loginRef, userRef]);
  
  // ここで非同期処理を実行
  const [result, isLoading] = stepsRef.read();
  
  const loginStatusResult = result[0];
  const userResult = result[1];

  /* -- 以下、描画処理 -- */
}

steps関数という関数が新たに登場しました!

この関数に非同期処理のrefを配列で渡すことで、配列の順番通りに渡した非同期処理を実行する事が出来ます。

そして、処理の流れが一つの非同期処理を実行する時と同じなので、覚えることが少なくて済みますし、分かりにくいですが、上記で指摘した問題点もTrela内部で解決してくれているので、安全に直列処理を実行する事が出来ています。

ここまでで直列処理が分かったら、今度は並列処理の例を見てきましょう。

並列処理 : Trelaを使ってない場合

const UserCard = ({ uid }) => {
  const [isLoading, setLoadingStatus] = useState(false);
  const [resultData, setResult] = useState({
    isLogin: false,
    user: defaultUserValue
  });

  useEffect(() => {
  
    (async () => {
      setLoadingStatus(true); // ローディング開始
      
      // fetchLoginStatus と fetchUser を並列処理
      const results = await Promise.all([
        fetchLoginStatus(uid),
        fetchUser(uid),
      ])
      
      setResult({ isLogin: results[0], user: results[1] }); // 通信結果をViewに反映
      setLoadingStatus(false); // ローディング終了
    })();
  
  }, [])

  /* -- 以下、描画処理 -- */
}

直列処理の時と大体一緒ですが、Promise.all関数を使って二つの非同期処理を並列処理しているため、ソースコードが少し冗長になってしまいました。

勿論、前項で指摘した問題点も解決できていません。
ただでさえ問題点を抱えているのに、ソースコードまで冗長になってしまうのはあまり良くありませんね。

Trelaを使った例を見てみましょう。

並列処理 : Trelaを使った例

const UserCard: React.FC<{ uid: number }> = ({ uid }) => {
  const { apis, all } = useTrela();
  
  // 非同期処理のRefを取得( まだ非同期処理はやってない )
  const loginRef = apis.fetchLoginStatus().default(false);
  const userRef = apis.fetchUser().default(defaultUserValue);

  // fetchLoginStatus と fetchUser を並列に実行するRefを取得
  const stepsRef = all([loginRef, userRef]);
  
  // ここで非同期処理を実行
  const [result, isLoading] = stepsRef.read();
  
  const loginResult = result[0];
  const userResult = result[1];

  /* -- 以下、描画処理 -- */
}

上記が並列処理のソースコードになりますが、ほとんど直列処理のソースコードと同じになっています!

直列処理との違いはsteps関数のところが、all関数に変わっただけです!

これによって、Trelaを使ってない時に比べて書き方が統一されているので、とても読み易く、修正しやすいコードになっています。そして、それに加えてより複雑な処理も書くことが出来ます。

以下のソースコードを見てください!

const UserCard: React.FC<{ uid: number }> = ({ uid }) => {
  const { apis, all, steps } = useTrela();
    
  const ref_1 = apis.fetch1();
  const ref_2 = apis.fetch2();
  const step_1_2 = steps([ref_1, ref_2]); // ref_1 -> ref_2の順に直列するRefを取得

  const ref_3 = apis.fetch3();
  const ref_4 = apis.fetch4();
  const step_3_4 = steps([ref_3, ref_4]); // ref_3 -> ref_4の順に直列するRefを取得

  const all_ref = all([ step_1_2, step_3_4 ]); // 二つの直列処理を並列実行するRefを取得

  const [result, isLoading] = all_ref.read(); // ここで非同期処理を実行
  
  /* -- 以下、描画処理 -- */
}

上記のソースコードは、ref_1 -> ref_2ref_3 -> ref_4と実行する直列処理二つを並列に実行しています。

この処理をTrelaを使わないでやろうとすると、とても複雑なソースコードになってしまいますが、Trelaを使うとシンプルで分かりやすく、修正しやすいコードになっています。

キャンセルだってできる。そう、Trelaならね。

Trelaでは、非同期処理をキャンセルすることが出来ます。
これは一つの非同期処理の時は勿論、直列処理並列処理に対しても出来るようになっていますので、大変便利な機能となっています。

キャンセルするには以下のようにします。

const Component = () => {
  const { apis, all, steps } = useTrela();

  const singleRef = apis.SampleApi(); // 非同期処理のRefを取得
  
  singleRef.cancel(); // 非同期処理をキャンセルする

  const stepRef = steps([ singleRef ]); // 直列処理のRefを取得する

  stepRef.cancel(); // 直列処理をキャンセルする

  const allRef = all([ singleRef, stepRef ]); // 並列処理のRefを取得

  allRef.cancel(); // 並列処理をキャンセルする

 /* -- 省略 -- */
}

上記のようにcancel関数を実行すれば、それだけで非同期処理をキャンセルできます。

ただ注意点として、Trelaでは実際の非同期処理をキャンセルしているのでなく、実行中の非同期処理から結果を受け取らないようにしているだけです。そのため、通信処理やバックグランド処理は動き続けたままになります。

※ ただ、ちゃんとキャンセル出来るようなAPIを実装予定なので、今後のバージョンアップで実行中の非同期処理をちゃんとキャンセル出来るようになります。

すべてが最適化されている。そう、Trelaならね。

さて、Trelaのメリットや特徴をここまで紹介してきましたが、実はTrelaの真骨頂はここからです!

それを見ていきましょう!

コンポーネントの描画更新の最適化

ここまでの解説で、Trelaが提供するuseTrelaというReact Custom Hooksを用いて、非同期処理を実行してきました。この時の非同期処理が完了するとTrelaが勝手に描画更新をしてくれるのですが、実はこの時に描画更新の最適化をしてくれています。

どういう事か見てきましょう。以下のソースコードを見てください。

const useSample = (): string => {
  const [result, setId] = useState<string>("");

  useEffect(() => {
    // fetchId関数はPromiseをキャッシュするので、無駄な通信はしない
    // なので、一回だけIDを取得する処理をする
    fetchId().then(setId); 
  }, []);

  return result;
}

const ComponentA = () => {
  const id = useSample();
 
  return (
   <p>あなたのIDは : {id} です</p>
  );
}

const Root = () => {
  const id = useSample();
 
  return (
    <div>
      <p>あなたのIDは : {id} です</p>
  
      <ComponentA />
    </div>
  );
}

突っ込みどころの多いコードですが( 大目に見てください )、上記のコードには無駄な描画更新が発生する可能性があります。
具体的には以下の流れです。

  1. Rootが実行される( マウントはされてない )
  2. ComponentAがマウントする
  3. ComponentAのuseEffect関数内でfetchIdが実行される
  4. Rootがマウントする
  5. RootのuseEffect関数内でfetchIdが実行される
  6. ComponentAのfetchIdの処理が終わり描画更新される( ComponentAは一回描画更新した )
  7. RootのfetchIdの処理が終わり描画更新される( Rootは一回描画更新した )
  8. ComponentAが描画更新された( ComponentAは二回描画更新された )

上記の流れで、<ComponentA />は二回描画更新[1]されましたが、これが無駄な描画更新です。 何故なら、二回目の描画更新では<ComponentA />は何も変化がないからです。

ここで、「 React.jsでは差分描画するから大丈夫では? 」と思った人もいる事でしょう。

実際に上記の例でも、React.jsの差分描画によってリアルなDOMに変更はありません。しかし、ComponentA関数は実行されるので、差分を確認するためのオブジェクト生成や、比較する処理が走るため、リソースは消費します。そのため、大きな構造を持つコンポーネントはリアルDOMに反映されなくても、とても重い処理となってしまう可能性があります。

そして、巨大なアプリや多くのデータを表示するアプリ( 台帳アプリなど )などの大きな構造を持ちやすいモノほど、複雑な描画更新フローになりやすいので、上記のような事が発生しやすいです。 ※ 小さいアプリでも、描画更新が多いと同じような問題が起こります。

しかし、Trelaではコンポーネントの親子構造を解析して、描画更新を最小限に抑えます。 そのため、Trelaを使えば上記の例の<ComponentA />を一回の描画更新に抑えることが出来ます。

const useSample = (): string => {
  const { apis } = useTrela();
  const ref = apis.fetchId().default(""); // 非同期処理のRefを取得

  const [result] = ref.read(); // ここで非同期処理を実行

  return result;
}

const ComponentA = () => {
  const id = useSample();
 
  return (
   <p>あなたのIDは : {id} です</p>
  );
}

const Root = () => {
  const id = useSample();
 
  return (
    <div>
      <p>あなたのIDは : {id} です</p>
  
      <ComponentA />
    </div>
  );
}

上記のソースコードでは、<ComponentA />描画更新は一回だけになります。 しかし、ソースコードにはそのような事が分かるような記述はありません。

そうです!ここがTrelaの真骨頂です!

Trelaを使うと、勝手に描画更新を最適化してくれます。 そのためTrelaでは、わざわざuseMemouseCallbackReact.memoなどを使って描画更新を抑える必要はありません。
どの情報が必要なのかを宣言するだけでいいんです。

これによって、プログラマーはよりやりたいことに専念することが出来ると思います。

通信の最適化

Trelaは、通信の処理も勝手に最適化してくれます。

例を見てみましょう。

const ListItem = ({ item }) => {
  const [user, setUser] = useState({ name : "" });

  useEffect(() => {
    // getUser関数の定義は省略してます。
    getUser(item.userId).then(setUser);  // 初マウント時一回だけ通信処理をする
  },[]);

  return (
    <li>
      <p>{item.name} @ create by {user.name}</p>
    </li>
  );
}

const App = () => {
  // userIdを持つオブジェクトを含む配列
  const items = [{ id:0, name:"hoge", userId:1 }, { id:1, name:"hogu", userId: 1 }];

  return (
    <ul>
      {
         items.map(item => (
           <ListItem key={item.id} item={item} userName={users[item.userId]} />
         ))
      }
    </ul>
  );
}

上記の例では、私が前に書いた記事の例を持ってきました。

やっている事は、配列から<ListItem />を複数描画しています。
<ListItem />は、初回マウント時にユーザーを取得する処理をしていますが、この時itemsの要素(item)の中に同じuserIdを持つitemが複数あります。そのため、同じユーザーを取得する処理を複数回(今回は二回)走らせてしまいます。

これはItemsのサイズが大きくなり、同じuserIdを持つitemが多くなれば、描画が遅くなったり、サーバーにも余計な負荷がかかるなどいろいろな問題が発生します。

しかし、TrelaではPromiseをキャッシュしているので無駄な通信を省くことが出来ます。

const ListItem = ({ item }) => {
  const { apis } = useTrela();
  const ref = apis.getUser().default({ name : "" }); // 通信処理のRefを取得

  const [result] = ref.read(); // 通信処理を実行

  return (
    <li>
      <p>{item.name} @ create by {user.name}</p>
    </li>
  );
}

const App = () => {
  // userIdを持つオブジェクトを含む配列
  const items = [{ id:0, name:"hoge", userId:1 }, { id:1, name:"hogu", userId: 1 }];

  return (
    <ul>
      {
         items.map(item => (
           <ListItem key={item.id} item={item} userName={users[item.userId]} />
         ))
      }
    </ul>
  );
}

勿論、使う側は通信の最適化について何もする必要はありません。ただ取得したいという事を宣言するだけですので、上記のようにシンプルに記述できています。

Store管理だって簡単にできる。そう、Trelaならね。

この機能はまだ開発中です。

Store管理を可能にするStore APIは、2020/09現在ではまだ制作途中ですが考え自体は既にあるのでご紹介します。
既存のライブラリと比較しながらだと分かりやすいと思いますので、この記事ではReduxRecoilのアプローチと比較しながらご紹介したいと思います。

Reduxのアプローチ

redux data flow

※ 簡略化のためActionCreatorの部分は省略しています。ご了承ください。

Reduxのアプローチは、複雑なデータフローをシンプルなデータフローに強制し、状態を一元管理しています。それによって、テスト・デバッグがしやすく、データの復元が容易にでき、複数の状態を伴った計算などもやり易いです。一方で、SotreReducerが肥大化して扱いづらくなったり、非同期処理がミドルウェアを使っても大変だったりします。

Reduxの良い所

  • データフローが分かりやすい
  • 状態管理がしやすい
  • テストやデバッグがしやすい

Reduxの良くない所

  • 記述が多い
  • 非同期処理がミドルウェア使っても大変
  • 巨大なデータを一元管理するのは大変( 分割して柔軟に処理したい )

Recoilのアプローチ

recoil data flow

最近何かと話題のRecoilですが、正直グラフにするのが難しかったので上記の画像はあまり参考にはならないと思います。なので、全体の雰囲気だけ理解してもらえると良いと思います。

Recoilのアプローチは、コンポーネントと状態を強く紐づけて、影響範囲を小さくするという感じです。これによって、影響範囲が小さくて済み、無駄な描画更新が発生しません。また状態を細かく分割して組み合わせる事で、柔軟な状態管理が可能です。 非同期処理にも対応しているので、Reduxよりも高性能で扱いやすい印象です。

一方、懸念点としてはatomselectorの数が多くなってしまい複雑化しやすいと思いますし、selectorsetが気を付けて使わないとより複雑さを招いてしまいそうな感じです。

ただ、まだまだ開発中のモノなので、これからのアップデートで変わっていくと思いますし、facebookが作っているというのは、とても安心感があると思います。

Recoilの良い所

  • 描画効率が良い
  • 柔軟なState管理が可能
  • 非同期処理が扱える( React Concurrent Modeに依存していますが、時間の問題だと思います )
  • facebookが作っているので、Reactとの相性は良い

Recoilの良くない所

  • データフローが複雑( グラフの雰囲気から分かると思います )
  • 記述量が多くなる( 場合によっては、Reduxより多くなると思う )
  • atomselectorの数が多くなって複雑化する( keyの指定はどうにかならないのか? )

Trelaのアプローチ

trela data flow

Trelaのアプローチはとても単純です。ViewAPIsのやり取りを横取りして新しいState作るという感じです。

そのため、基本的にViewAPIs直接的にStoreの処理に関与しません。 一方、StoreViewAPIsのやり取りを受け取るのみで、やり取り自体に関与しません。 また、出力もサブスクリプションしているコンポーネントにだけ反映するため影響範囲も少ない事が特徴です。

また、グラフからは分かりにくいですが、StoreRecoilselectorみたいな感じに、定義・使用できるような作りにしようと思っているので、Reduxのような一元管理ではなく、分散型のState管理だと思ってください。

今はまだ構想段階で、具体的な処理などはありませんが、最終的にはReduxRecoilの両方の性質を併せ持ったモノにしたいと思ってます。

Trela Storeの良い所

  • データフローが単純
  • 他の処理に影響を出さない
  • 影響範囲を限定できる
  • ReduxRecoilを足して2で割って1を足した感じ

Trela Storeの良くない所

  • 入出力が限定されており、扱いにくい
  • Reduxのような一元管理は難しいかも
  • Recoilほどの柔軟性は無いかも

React Concurrent Modeに対応できる。そう、Trelaならね。

この機能はまだ開発中です。

React Concurrent Modeは、Reactで非同期処理を扱える新しいAPIになります。現在( 2020/09 )ではまだ実験段階ですが、おそらくこれからのReactで非同期処理を扱うのによく使われるようになると思います。

Trelaではその事を見越して、今まで紹介したソースコードを崩さずにReact Concurrent Modeに対応する事が出来ます!

具体的には以下のようにします。

const { Provider, useTrela, TrelaSuspence } = createTrelaContext({ apis: { fetchUserList } });

const UserList = () => {
  const { apis } = useTrela();
  const [users, isLoading] = apis.fetchUserList.read(); // ここで非同期処理をする

  if( isLoading ) return <div className="loading-bar" />

  return (
    <ul className="user-list">
      { 
        users.map(user => (
          <li key={user.id} className="user-list-item">{ user.name }</li>
        ))
      }
    </ul>
  );
}

const App = () => (
  <TrelaSuspence fallback={<div className="loading-bar" />}>
    <UserList />
  </TrelaSuspence>
);

<UserList />は、今まで紹介してきたTrelaの機能を使って非同期処理を実行しています。

この<UserList /><TrelaSuspence />という新しく出てきたTrelaが提供するコンポーネントで囲った上げるだけで、その部分のみReact Concurrent Modeとして実装されます!

これによって、React Concurrent Modeを段階的に導入したり、部分的に使用することが可能になります!

このAPIの良い所は、React Concurrent Modeの実装と従来の実装の切り替えが簡単な所にあります。
もしReact Concurrent Modeが嫌になったら、<TrelaSuspence />を削除するだけなので、<UserList />には何も変更を加える必要はありません。**

これによって、<UserList />をより柔軟に扱う事が可能になります!

Anyなんて言わせない。そう、Trelaならね。

TrelaはTypeScriptで作っているため、TypeScriptによる型推論の恩恵を多く受け取ることが出来ます。例えば、apisで非同期処理のRefを取得する時などは、入力保管が効いていてとても便利です。

trela typing

また、非同期処理に引数を渡す時もちゃんと型が付いているので、型安全に非同期処理を行えます。

Let's TS Hack!

上記のメリットをより多く受け取るために、ちょっとしたTypeScriptの書き方をご紹介します。

まず初めにTypeScirptのすごい所として、ファイル内の変数を全てimportするとそのimportした変数の型推論をやってくれるところがあります。

// ./test.ts

export const Hello = "test";

export const Hoge = 0;
// ./index.ts

import * as AllValue from "./test.ts";

type AllValueType = typeof AllValue;
/*
  AllValueの型を持ってくることができる
  {
     Hello: string;
     Hoge: number;
  }
*/

これを利用すると疎結合なソースコードをTrelaを使って書くことが出来ます。
その例をこれから見てきます。まずはファイル構造を見てましょう。

  ├ apis.ts <- Api関数を定義するファイル
  ├ context.ts <- TrelaのContextをexportするファイル
  └ component.tsx <- コンポーネントを定義するファイル

次に上記のapis.tsファイルにAPI関数を定義していきます。

// ./apis.ts

// 渡されたUserIdのユーザー情報を取得するAPI
export const fetchUser = async (id: number): Promise<{ id: number; name: string; }> => {
  const ref = await fetch(`https://example.com/api/user/${id}`);
  const json = await ref.json();

  return { id, name: json.name }
}

API関数が出来たら、それを使ってTrelaのContextを作ります。
Contextは別ファイルに書くと、後はimportして使うだけになって、ほとんど変更しなくて済むのでお勧めです。

// ./context.ts

import { createTrelaContext } from "trela";
import * as apis from "./apis"; // import * as apis ~ としているのがポイント!

// apis.tsで定義しexportした関数をcreateTrelaContextに渡す
const { Provider, useTrela } = createTrelaContext({ apis });

export {
  Provider, // Trelaが提供するProvierコンポーネント( Rootコンポーネントとして指定する必要があります )
  useTrela // Trelaが提供するReact Custom Hooks関数
};

Contextが定義出来たら、上記でexportしたモノをコンポーネントで使います。
以下のソースコードは、UserIdを使ってUser情報を取得し、<UserCard />を表示しています。

// ./component.tsx

import React from "react";
import ReactDOM from "react-dom";
import { Provider, useTrela } from "./context"; 

// 渡されたIDを持つUserをロードして表示する
const UserCard:React.FC<{ id: number }> = ({ id }) => {
  const { apis } = useTrela();
  const [user, isLoading] = apis.fetchUser(id).read(); // ここで非同期処理を実行している

  if(isLoading) return <div className="loading">通信中です</div>

  return (
    <div className="user-card">
       <div className="card-label">{ user?.name }</div>
    </div>
  );
}

const App = () => {
  const userIds = [ 1, 2, 3 ]; // 取得するユーザーのID

  return (
    <div className="app">
      {
        userIds.map((id) => (
          <UserCard id={id} key={id} />
        ))
      }
    </div>
  );
}


ReactDOM.render(
  <Provider>
    <App />
  </Provider>,
  document.getElementById("root")
);

ここまでは普通のTrelaの使い方ですね。
上記のソースコードのapis.tsfetchFollowersという新しいAPIをapis.tsに追加してみます。

// ./apis.ts にfetchFolloers関数を追加

// 渡されたUserIdのユーザー情報を取得するAPI
export const fetchUser = async (id: number): Promise<{ id: number; name: string; }> => {
  const ref = await fetch(`https://example.com/api/user/${id}`);
  const json = await ref.json();

  return { id, name: json.name }
}

// フォロワーを取得するAPI関数
export const fetchFollowers = async (user_id: number) => {
  const ref = await fetch(`https://example.com/api/followers/${user_id}`);
  const json = await ref.json();

  return json as Array<{ name: string }>;
}

そして、fetchFollowers<UserCard />で使うには以下のようにします。

// ./component.tsx でfetchFollowers関数を使う

import React from "react";
import ReactDOM from "react-dom";
import { Provider, useTrela } from "./context"; 

// 渡されたIDを持つUserをロードして表示する
const UserCard:React.FC<{ id: number }> = ({ id }) => {
  const { apis } = useTrela();
  const [user, isLoading] = apis.fetchUser(id).read(); // ここで非同期処理を実行している

  // fetchFollowersを追加
  const [followers] apis.fetchFollowers(id).default([]).read();

  if(isLoading) return <div className="loading">通信中です</div>

  return (
    <div className="user-card">
      <div className="card-label">{ user?.name }</div>

      <ul>
        {
          followers.map((follower, i) => (
            <li key={`follower_${i}`}>{ follower.name }</li>
          ))
        }
      </ul>
    </div>
  );
}

/* -- 以下、変更が無いので省略 -- */

ここで注目すべき点は、components.tsのimport部分は全く変更しなくてもfetchFollowersを使えている所です。これによって、API部分とAPIを使う部分が疎結合となっているので、修正箇所が少なくて済みますし、ソースコードが見やすいと思います。勿論、上記で示したようにTypeScriptのサポートも受けることが出来ます。

TS Hack発展形

また、これを発展させてapis.tsをフォルダーにしてfetchUserfetchFollowersを別ファイルにしてみましょう。

まずはフォルダー構造は以下のようになります。

  ├ apis  <- フォルダーに変更
  │ ├ index.ts 
  │ ├ user.ts
  │ └ follower.ts 
  ├ context.ts <- TrelaのContextをexportするファイル
  └ component.tsx <- コンポーネントを定義するファイル

今度は上記の新しく作ったファイルを定義していきます。

// ./apis/index.ts

export * from "./user" // user.ts の export しているものすべてを export
export * from "./folloer"; // follower.ts の export しているものすべてを export
// ./apis/user.ts

// 渡されたUserIdのユーザー情報を取得するAPI
export const fetchUser = async (id: number): Promise<{ id: number; name: string; }> => {
  const ref = await fetch(`https://example.com/api/user/${id}`);
  const json = await ref.json();

  return { id: number, name: json.name }
}
// ./apis/follower.ts

// 渡されたUserIdをフォローしているUser情報を取得するAPI
export const fetchFollowers = async (id: number): Promise< Array<{ id: number; name: string; }> > => {
  const ref = await fetch(`https://example.com/api/followers/${id}`);
  const json = await ref.json();

  return json as Array<{ id: number; name: string; }>;
}

上記のようにファルダー構造を変更しても、context.tscomponents.tsには何の変更もありません。

これによりAPIの種類によってファイルを分割して書けるので、よりソースコード管理が簡単になりますので是非ご活用ください!

あなたでも貢献できる。そう、Trelaならね。

TrelaはOSSとして開発していますので、誰でも貢献することが出来ます。
勿論、IssuesやPull Requestを使って貢献してもらいたいですが、それ以外にも貢献方法はあると私は思っています。
この際なので、そのことについてちょっと言わせてください!

OSSを評価して貢献しよう!

OSSへの貢献でよく誤解されているのが、IssuesやPull Requestを出すことだけが貢献することだと思われている事です。しかし、これは大きな間違いです!OSSを評価することも立派なOSSへの貢献です!

例えば、Githubのスターを付ける事や、議論できる場でOSSについて肯定・否定することなど、自分なりの評価をしましょう。例えそれが信ぴょう性に欠けるものでも、みんながやる事で積もり積もって信頼できる評価に変わります! だから、ちゃんと評価してあげましょう。

ボタンを押すぐらい、今すぐにでも出来るはずですよね?

また、「 良いモノなら絶対に評価されているはずだ! 」などの愚考はしないで下さい。もし絶対に評価されるなら「 埋もれた名作 」なんて言葉はあるはずが無いですし、それを理由に評価しないという事にはなりません。

有名でも、無名でも、あなたなりの評価をすべきです。

そして、みんながちゃんと評価し合うようになれば、知られていないだけで物凄いライブラリーとかが出てくるかもしれません。それがあなたの貢献で見つかったのなら、それって凄く魅力的な事だと思いませんか? 私は思います :)

OSSを布教して貢献しよう!

貢献したいOSSの事を他の人に教えてあげることも立派なOSSへの貢献です。

  • SNSで貢献したいOSSの事をフォロワーに教える
  • 自分のブログや記事投稿サイトに貢献したいOSSの事を書く
  • 自分の仕事仲間や上司に教えてあげる
  • 誰かが布教したモノを、再度布教する

などなど、自分じゃない誰かに教えてあげることで、もしかしたら教えたその人がOSSに貢献してくれるかもしません。そうなったら、あなたは間接的にOSSに貢献していると言えますよね?だから、「 貢献したい! 」と思ったら、まず初めに誰かにその事を伝えてみると言うのが一番初めに出来る貢献だと思います。

また、布教の良い所は 技術も、意見も、知識も、何も無くても出来るところです!
評価することに抵抗がある人も、このくらいの貢献なら出来ると思います。

日本人なら日本人がやっているOSSに真っ先に貢献して!

これは単純な話です。

日本人の事を一番理解できるのは、日本人だからです。

そのOSSの開発者が日本人なら、同じ日本人である私たちの方がコミュニケーションはスムーズですね? それに同じ日本人だから、応援したいと思うのはダメなのでしょうか?

前に「 日本人は日本人に対して厳しい 」という話をどっかで聞いたことがあるのですが、昨今のIT業界を見ていると「 本当にそうだな 」と思う事が良くあります。特に、新しい事を挑戦的にやっている人に対して厳しい感じがします。 Winny事件はその最たる例でしょう。

前例を踏襲しているモノを評価する事もいいですが、新しい事に挑戦している事もちゃんと評価してあげる必要があると思います。特にIT業界によく触れ合っている私たちは、その重要性を理解しているはずです。

あとがき。そう、Trelaのね。

ここまで読んでくれてありがとうございます🙏

まだまだ開発中のモノなので色々とバグが多いと思いますし、コンポーネントの構造を動的に解析する部分は本当に良いアプローチなのか未だに分かってないので、誰か意見をくれると嬉しいです🙇‍♂️

実は、開発している最中にRecoilの発表があったので開発を辞めようかなと思っていたのですが、自分が思っていたよりRecoilに良さを感じなかったのと、「 Recoilの良い所を真似すればいいじゃない! 」という発想で開発を辞めませんでした。実際、Recoilのソースコードを何度も見て、どこかに参考となるような部分が無いか探していました。( Flowが見やすいエディタ誰か教えて。。。VSCodeは重すぎて私の環境だと無理。。。 )

あと、Recoilはデータフローが複雑になると思うのですが、これは問題にならないのでしょうか🤔? それにselectorsetは危ないような気がするのですが、そこら辺に詳しい人が居ましたらご教授いただければ幸いです。

OSSについては、お願いだからみんな日本のOSSを応援してあげて!
Trelaはもうこの際どうでもいいので、せめて他の人のプロジェクトに貢献してあげて😭
中国語のOSSは凄く活気があって楽しそうなのに、日本のOSSは活気が無いのを見てると、とっても悲しくなってくる。。。 いいねを押すとか、布教くらいなら、時間が無くても出来るでしょ?

言っておくけど、みんなが評価しないから、READMEとか日本語で書けないし、書きたくないんだよ! みんながちゃんと評価してくれるなら日本語でやろうと思えるし、多言語対応なんて必要になった時にやればいいし、まずは小さく作って、みんなで評価して、スピード感を持って大きく育てる必要があると思う。

いつまで海外の目を気にしたら気が済むの? もっと日本に目を向けて頑張るべきじゃない?

ぶっちゃけると、
「 なんで、英語でOSSしなきゃならんのさ!多様性の時代とはよく言ったモノだな😡 」
って、私はいつも思ってる。

( ´Д`)=3 フゥ

他にも言いたいことはいっぱいあるけど、それはまた別の機会にでも話します。

Trelaについて他に気になる点などがありましたら、コメントなどで教えてください。
それでは、また👋

脚注
  1. ここでの描画更新とはコンポーネント関数( クラスではrender関数 )を実行する事として扱います。分かりにくい言い方だとは思いますが、他に思いつかなかったのでこれで通します。ご了承ください。 ↩︎