lamechang-dev

Webフロントエンドエンジニア lamechangのブログ。

【TypeScript】【React】【Recoil】翻訳しながらそれなりに型をつけて細々と進めていくRecoil Tutorial その3 Asynchronous Data Queries編(1)

f:id:lamechang_dev:20220213113950p:plain

本記事は、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリである Recoil の公式チュートリアルを翻訳しつつ、TypeScriptで型付けをしながら進めていく連載の第三弾です。細々とやっていきます🙄 今回は Asynchronous Data Queries について。

recoiljs.org

Asynchronous Data Queries

Recoil provides a way to map state and derived state to React components via a data-flow graph. What's really powerful is that the functions in the graph can also be asynchronous. This makes it easy to use asynchronous functions in synchronous React component render functions. Recoil allows you to seamlessly mix synchronous and asynchronous functions in your data-flow graph of selectors. Simply return a Promise to a value instead of the value itself from a selector get callback, the interface remains exactly the same. Because these are just selectors, other selectors can also depend on them to further transform the data.

Selectors can be used as one way to incorporate asynchronous data into the Recoil data-flow graph. Please keep in mind that selectors represent "idempotent" functions: For a given set of inputs they should always produce the same results (at least for the lifetime of the application). This is important as selector evaluations may be cached, restarted, or executed multiple times. Because of this, selectors are generally a good way to model read-only DB queries. For mutable data you can use a Query Refresh or to synchronize mutable state, persist state, or for other side-effects consider the experimental Atom Effects API.


Recoilは、データフローグラフを介して状態と派生状態をReactコンポーネントマッピングする方法を提供します。本当に強力なのは、グラフの関数も非同期にできることです。これにより、同期Reactコンポーネントレンダリング関数で非同期関数を簡単に使用できるようになります。 Recoilを使用すると、セレクターのデータフローグラフで同期関数と非同期関数をシームレスに組み合わせることができます。セレクターの getコールバックから値自体ではなく、Promiseを値に返すだけで、インターフェースはまったく同じままになります。これらは単なるセレクターであるため、他のセレクターもデータをさらに変換するためにそれらに依存することができます

セレクタは、非同期データをRecoilデータフローグラフに組み込む1つの方法として使用できます。セレクタは idempotent(べき等) 関数を表すことを心に留めておいてください。特定の入力のセットの場合、それらは常に同じ結果(少なくともアプリケーションの有効期間)を生成する必要があります。セレクタ評価がキャッシュ、再起動、または複数回実行される可能性があるため、これは重要です。このため、セレクタは一般的に読み取り専用DBクエリをモデル化するための良い方法です。可変データの場合、クエリの更新を使用するか、可変状態を同期させることができます。

Synchronous Example

For example, here is a simple synchronous atom and selector to get a user name:


たとえば、これはユーザー名を取得するための単純なAtomSelectorです。


これは、非同期を含まない非常に単純な例ですネ。サンプルコードでは非同期を挟まないローカルデータが存在することを意図して記述されていたので、簡単なオブジェクトだけ追加して動作を確認しました。

// サンプルコードから追加
const tableOfUsers = [
  {
    name: "Jane",
  },
  {
    name: "Mike",
  },
];

// 以下は変更なし
const currentUserIDState = atom({
  key: "CurrentUserID",
  default: 1,
});

export const currentUserNameState = selector({
  key: "CurrentUserName",
  get: ({ get }) => {
    return tableOfUsers[get(currentUserIDState)].name;
  },
});

const CurrentUserInfo: React.VFC = () => {
  const userName = useRecoilValue(currentUserNameState);
  return <div>{userName}</div>;
};

export default CurrentUserInfo;

Asynchronous Example

If the user names were stored in some database we need to query, all we need to do is return a Promise or use an async function. If any dependencies change, the selector will be re-evaluated and execute a new query. The results are cached, so the query will only execute once per unique input.


ユーザー名がQueryを実行する必要があるDBに保存されている場合は、やることはPromiseを返すか、非同期を使用することだけです。依存関係が変更された場合、セレクタは再評価され、新しいクエリを実行します。結果がキャッシュされるため、Queryは一意の入力ごとに1回だけ実行されます。


実際のドキュメントでは、Promiseを返す myDBQuery()について特に具体例などがあげられていないので、json-serverを使うなどして簡易的なエンドポイントを作ればいいかと思います。一応、json-serverを利用して書いた myDBQuery()側のコードについても載せておきます。

// 特に変更点なし

export const currentUserNameQuery = selector({
  key: "CurrentUserName",
  get: async ({ get }) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    return response.name;
  },
});
// myDBQuery

import axios from "axios";

export const BASE_URL = "http://localhost:3001";

export const apiClient = axios.create({
  baseURL: BASE_URL,
  headers: {
    "Content-Type": "application/json",
  },
});

export type User = {
  id: number;
  name: string;
};

export const myDBQuery = async ({
  userID,
}: {
  userID: User["id"];
}): Promise<User> => {
  return apiClient
    .get<User>(`/users/${userID}`)
    .then((res) => res.data);
};
// json-serverで利用するJSON
// json-server --p 3001 --delay 1000 --watch path/to/xx.json
{
  "users": [
    {
      "id": 1,
      "name": "Jane"
    },
    {
      "id": 2,
      "name": "Mike"
    },
    {
      "id": 3,
      "name": "Cathy"
    }
  ]
}


The interface of the selector is the same, so the component using this selector doesn't need to care if it was backed with synchronous atom state, derived selector state, or asynchronous queries!


Selectorのインターフェースは同じであるため、このSelectorを使用したコンポーネントは、値が Atom か Selector、または非同期Queryで返されているかどうかなどを気にする必要がありません!


But, since React render functions are synchronous, what will it render before the promise resolves? Recoil is designed to work with React Suspense to handle pending data. Wrapping your component with a Suspense boundary will catch any descendants that are still pending and render a fallback UI:


しかし、ReactのRender関数は同期処理なので、Promiseが解決する前に何がレンダリングされるのでしょうか。Recoilは、Pendingデータを処理するために React Susenseで動作するように設計されています。Suspense boundaryコンポーネントをWrapすると、まだ保留中の子コンポーネントがキャッチされ、フォールバックUIをレンダリングします。

ここは実際にコードを書く際に、 <React.Suspense> をはずしてみて画面を読み込む際に何が起きるか、を見るのが一番わかりやすいと思います。以下のように「Suspense状態なのにfallback UIが指定されてないから<Suspense>コンポーネント使って指定してね」と怒られます。

f:id:lamechang_dev:20220211132126p:plain

<React.Suspense>コンポーネントを囲ってあげると、Promiseが解決されるのを待機している状態では以下のような挙動になります。

f:id:lamechang_dev:20220212130622g:plain


const App: React.FC = () => {
  return (
    <RecoilRoot>
       // React.Suspenseを外すとエラーになる
      <React.Suspense fallback={<div>Loading...</div>}>
        <CurrentUserInfo />
      </React.Suspense>
    </RecoilRoot>
  );
};
export default App;

Error Handling

But what if the request has an error? Recoil selectors can also throw errors which will then be thrown if a component tries to use that value. This can be caught with a React . For example:


しかし、リクエストにエラーがある場合はどうなりますか?RecoilのSelectorはまた、コンポーネントがその値を使用しようとしている場合にスローされるエラーをスローすることもできます。これはコンポーネント <ErrorBoundary>で捉えることができます。例えば

const currentUserNameQuery = selector({
  key: 'CurrentUserName',
  get: async ({get}) => {
    const response = await myDBQuery({
      userID: get(currentUserIDState),
    });
    if (response.error) {
      throw response.error;
    }
    return response.name;
  },
});

function CurrentUserInfo() {
  const userName = useRecoilValue(currentUserNameQuery);
  return <div>{userName}</div>;
}

function MyApp() {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <CurrentUserInfo />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
}

Recoil側のリファレンスでError Boundary についての詳細の説明がないのでこの記事においても詳細は省きますが、以下のような Error Boundary コンポーネントを用意して上記の例のようにWrapした上で、selectorの中でPromiseの解決に問題が発生した際などにErrorをthrowをすると、以下のような画面を表示することができます。

これは「クラッシュした UI を表示させる代わりに、フォールバック用の UI を表示するコンポーネントを準備して見せた方がユーザから見るとマシだよね」、といった感じですよね。

import React, { Component, ErrorInfo, ReactNode } from "react";

interface Props {
  children: ReactNode;
}

interface State {
  hasError: boolean;
}

class ErrorBoundary extends Component<Props, State> {
  public state: State = {
    hasError: false
  };

  public static getDerivedStateFromError(_: Error): State {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
    console.error("Uncaught error:", error, errorInfo);
  }

  public render() {
    if (this.state.hasError) {
      return <h1>Sorry.. there was an error</h1>;
    }

    return this.props.children;
  }
}

export default ErrorBoundary;

f:id:lamechang_dev:20220212124540p:plain

Queries with Parameters

Sometimes you want to be able to query based on parameters that aren't just based on derived state. For example, you may want to query based on the component props. You can do that using the selectorFamily helper:


派生状態に純粋に基づいていないパラメータに基づいてクエリしたい場面があるでしょう。たとえば、コンポーネントのPropsに基づいてQueryしたい時が出てくるでしょう。selectorFamilyヘルパーを使用してそれをすることができます。


このatomFamilyselectorFamily の概念は実際の現場ではかなり重宝する印象です。というのも

仮に「id=”1″ のUserの情報」を表示して「id=”2″のUserの情報」に切り替えたい時に、もしatomだけを愚直に使うなら、切り替える際にatomの状態をいったんクリアするような処理が必要となってしまいますが、そのようなことはする必要なくなりますね。

型を定義する上でのポイントとしては、selectorFamilyは型引数を2つ受け付けるようになっていて、1つ目については selector と完全に同じものですが、2つ目については get()で利用するパラメータに対して型を付けることができます。

export const userNameQuery = selectorFamily<string, number>({
  key: 'UserName',
  get: userID => async () => {
    const response = await myDBQuery({ userID });
    if ("isAxiosError" in response)
    {
      throw response.message
    }
    return response.name;
  },
});
const UserInfo: React.VFC<{ userID: number }> = ({ userID }) => {
  // 引数を受け付ける
  const userName = useRecoilValue(userNameQuery(userID));
  return <div>{userName}</div>;
}

export default UserInfo
const App: React.VFC = () => {
  return (
    <RecoilRoot>
      <ErrorBoundary>
        <React.Suspense fallback={<div>Loading...</div>}>
          <UserInfo userID={1}/>
          <UserInfo userID={2}/>
          <UserInfo userID={3} />
        </React.Suspense>
      </ErrorBoundary>
    </RecoilRoot>
  );
};
export default App;

Data-Flow Graph

Remember, by modeling queries as selectors, we can build a data-flow graph mixing state, derived state, and queries! This graph will automatically update and re-render React components as state is updated.


SelectorとしてQueryをモデル化することで、データフローグラフのミックスされた状態、派生状態、およびクエリを構築できます。このグラフは自動的にReact Componentを更新して更新して再レンダリングします。


The following example will render the current user's name and a list of their friends. If a friend's name is clicked on, they will become the current user and the name and list will be automatically updated.


次の例では、現在のユーザーの名前とその友達のリストを表示します。友達の名前をクリックすると、その友達が現在のユーザーになり、名前とリストが自動的に更新されます。

コードを読むと気づくことですが、各 UserfriendList を持つことを想定しているようなので、 型 Userを以下のように調整してあげましょう。

export type User = {
  id: number;
  name: string;
  // friendListを追加
  friendList: Array<User["id"]>
};

その上でチュートリアルのコードを型をつけて書いていくと以下のようになります。やはり、型引数のおかげで各 selector の挙動の見通しがかなり分かりやすいですね。

export const currentUserIDState = atom({
  key: "CurrentUserID",
  default: 1,
});

export const userInfoQuery = selectorFamily<User, number>({
  key: 'UserInfoQuery',
  get: userID => async () => {
    const response = await myDBQuery({userID});
    if ("isAxiosError" in response) {
      throw response.message;
    }
    return response;
  },
});

export const currentUserInfoQuery = selector<User>({
  key: 'CurrentUserInfoQuery',
  get: ({get}) => get(userInfoQuery(get(currentUserIDState))),
});

export const friendsInfoQuery = selector<Array<User>>({
  key: 'FriendsInfoQuery',
  get: ({get}) => {
    const {friendList} = get(currentUserInfoQuery);
    return friendList.map(friendID => get(userInfoQuery(friendID)));
  },
});
import { useRecoilValue, useSetRecoilState } from "recoil";
import { currentUserInfoQuery, friendsInfoQuery, currentUserIDState } from "../../Global/User";

const CurrentUserInfo: React.VFC = () => {
  const currentUser = useRecoilValue(currentUserInfoQuery);
  const friends = useRecoilValue(friendsInfoQuery);
  const setCurrentUserID = useSetRecoilState(currentUserIDState);
 return (
    <div>
      <h1>{currentUser.name}</h1>
      <ul>
        {friends.map(friend =>
          <li key={friend.id} onClick={() => setCurrentUserID(friend.id)}>
            {friend.name}
          </li>
        )}
      </ul>
    </div>
  );};

export default CurrentUserInfo;

記事が長くなりそうなので、Concurrent Requests 以降については別の記事として切り出そうと思います。Reactのdocは全般的に翻訳しようとするとルー大柴現象が起きますね。

【TypeScript】【React】【Recoil】翻訳しながらそれなりに型をつけて細々と進めていくRecoil Tutorial その2 Selector編

f:id:lamechang_dev:20220213113823p:plain

本記事は、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリである Recoil の公式チュートリアルを翻訳しつつ、TypeScriptで型付けをしながら進めていく連載の第二弾です。細々とやっていきます🙄 今回は Selector について。

recoiljs.org

Selector

A selector represents a piece of derived state. You can think of derived state as the output of passing state to a pure function that modifies the given state in some way.

Derived state is a powerful concept because it lets us build dynamic data that depends on other data. In the context of our todo list application, the following are considered derived state:

Filtered todo list: derived from the complete todo list by creating a new list that has certain items filtered out based on some criteria (such as filtering out items that are already completed).

Todo list statistics: derived from the complete todo list by calculating useful attributes of the list, such as the total number of items in the list, the number of completed items, and the percentage of items that are completed.

To implement a filtered todo list, we need to choose a set of filter criteria whose value can be saved in an atom. The filter options we'll use are: "Show All", "Show Completed", and "Show Uncompleted". The default value will be "Show All":


セレクターは、派生状態の一部を表します。派生状態は、特定の状態を何らかの方法で変更する純粋関数に状態を渡す出力と考えることができます。

派生状態は、他のデータに依存する動的データを構築できるため、強力な概念です。 ToDoリストアプリケーションのコンテキストでは、以下は派生状態と見なされます。

フィルタリングされたToDoリスト: いくつかの基準に基づいて特定のアイテムが除外された新しいリストを作成することにより、完全なToDoリストから派生します(すでに完了しているアイテムを除外するなど)。

ToDoリストの統計:リスト内のアイテムの総数、完了したアイテムの数、完了したアイテムの割合など、リストの有用な属性を計算することにより、完全なToDoリストから導出されます。

フィルター処理されたToDoリストを実装するには、値をアトムに保存できるフィルター基準のセットを選択する必要があります。使用するフィルターオプションは、「すべて表示」、「完了済みを表示」、「未完了を表示」です。デフォルト値は「すべて表示」になります。

「すべて表示」、「完了済みを表示」、「未完了を表示」がフィルターオプションになるとのことなので、それらの条件を文字列リテラルのユニオンで定義してあげようと思います。すると、コードは以下のようになるかと思います。

export type TodoListFilterType =
  | "Show All"
  | "Show Completed"
  | "Show Uncompleted";

export const todoListFilterState = atom<TodoListFilterType>({
  key: "todoListFilterState",
  default: "Show All",
});

 

Using todoListFilterState and todoListState, we can build a filteredTodoListState selector which derives a filtered list:


todoListFilterStateとtodoListStateを使用して、フィルターされたリストを導出するfilteredTodoListState Selectorを構築できます。

selectorは以下のようなコードになります。ここで前回のatomの時と同様に、selector が保持する型を明らかにするためにジェネリクスを使っています。

またtodoListFilterStateの型を文字列リテラルで指定したため、switch文で利用しているcaseに意図しない文字列リテラルが含まれるとエラーを吐くようになることで安全に記述できています。

const filteredTodoListState = selector<Todo[]>({
  key: "filteredTodoListState",
  get: ({ get }) => {
    const filter = get(todoListFilterState);
    const list = get(todoListState);

    switch (filter) {
      case "Show Completed":
        return list.filter((item) => item.isComplete);
      case "Show Uncompleted":
        return list.filter((item) => !item.isComplete);
      default:
        return list;
    }
  },
});

 

The filteredTodoListState internally keeps track of two dependencies: todoListFilterState and todoListState so that it re-runs if either of those change.


filteredTodoListStateは、todoListFilterStateとtodoListStateの2つの依存関係を内部的に追跡し、これらのいずれかが変更された場合に再実行されるようにします。

 

From a component's point of view, selectors can be read using the same hooks that are used to read atoms. However it's important to note that certain hooks only work with writable state (i.e useRecoilState()). All atoms are writable state, but only some selectors are considered writable state (selectors that have both a get and set property). See the Core Concepts page for more information on this topic.


Componentの観点からは、Atomの読み取りに使用されるのと同じフックを使用してセレクターを読み取ることができます。ただし、特定のフックは書き込み可能な状態(つまり、useRecoilState())でのみ機能することに注意することが重要です。すべてのアトムは書き込み可能な状態ですが、一部のSelectorのみが書き込み可能な状態と見なされます(getプロパティとsetプロパティの両方を持つセレクター)。このトピックの詳細については、「コアコンセプト」ページを参照してください。

ここの部分、つまるところSelector には2通りの形式があって、一部は書き込み可能な状態と扱われてそれ以外はそうならない、ということになります。これは selector()の型定義を見ると分かりやすかったです。以下のように2種類の型定義が存在し、RecoilValueReadOnly<T> を返すケース(getプロパティのみを持つケース)が今回定義したtodoListFilterState に該当します。

export function selector<T>(options: ReadWriteSelectorOptions<T>): RecoilState<T>;
export function selector<T>(options: ReadOnlySelectorOptions<T>): RecoilValueReadOnly<T>;

 

Displaying our filtered todoList is as simple as changing one line in the TodoList component:


フィルタリングされたtodoListの表示は、TodoListコンポーネントの1行を変更するのと同じくらい簡単です。

ふむふむ、といった感じです。Vueを書いたことがある人なら、selectorのコンセプトはVueのcomputedにそっくりだと感じると思います。

const TodoList: React.VFC = () => {
  // changed from todoListState to filteredTodoListState
  const todoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <TodoItemCreator />
      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
};

 

Note the UI is showing every todo because todoListFilterState was given a default value of "Show All". In order to change the filter, we need to implement the TodoListFilters component:


todoListFilterStateにはデフォルト値の「ShowAll」が指定されているため、UIにはすべてのtodoが表示されていることに注意してください。フィルタを変更するには、TodoListFiltersコンポーネントを実装する必要があります。

const TodoListFilters: React.VFC = () => {
  const [filter, setFilter] = useRecoilState(todoListFilterState);

  const updateFilter = (e: ChangeEvent<HTMLSelectElement>) => {
    setFilter(e.target.value as TodoListFilterType);
  };

  return (
    <>
      Filter:
      <select value={filter} onChange={updateFilter}>
        <option value="Show All">All</option>
        <option value="Show Completed">Completed</option>
        <option value="Show Uncompleted">Uncompleted</option>
      </select>
    </>
  );
};

ここまでの実装で、以下のような画面になります。フィルタリング機能が追加されましたね。

f:id:lamechang_dev:20220205181238p:plain



With a few lines of code we've managed to implement filtering! We'll use the same concepts to implement the TodoListStats component.

We want to display the following stats:

・Total number of todo items
・Total number of completed items
・Total number of uncompleted items
・Percentage of items completed

While we could create a selector for each of the stats, an easier approach would be to create one selector that returns an object containing the data we need. We'll call this selector todoListStatsState:


いくつかのコードを使用して、フィルタリングを実装することができました! ToDoListStatsコンポーネントを実装するために同じ概念を使用します。

次の統計を表示したいと思います。

・TODOアイテムの総数
・完成したアイテムの総数
・完成していないアイテムの総数
・完了したアイテムの割合

統計ごとにセレクターを作成することもできますが、より簡単な方法は、必要なデータを含むオブジェクトを返す1つのセレクターを作成することです。このセレクターをtodoListStatsStateと呼びます。

ここは特筆すべきところはないですが、こういった類の統計値を使いたい場合は、依存先が共通であるのであれば同じ selector の中でまとめて返してあげることもできるよ、といったところですかね・・

const todoListStatsState = selector<{
  totalNum: number;
  totalCompletedNum: number;
  totalUncompletedNum: number;
  percentCompleted: number;
}>({
  key: "todoListStatsState",
  get: ({ get }) => {
    const todoList = get(todoListState);
    const totalNum = todoList.length;
    const totalCompletedNum = todoList.filter((item) => item.isComplete).length;
    const totalUncompletedNum = totalNum - totalCompletedNum;
    const percentCompleted =
      totalNum === 0 ? 0 : (totalCompletedNum / totalNum) * 100;

    return {
      totalNum,
      totalCompletedNum,
      totalUncompletedNum,
      percentCompleted,
    };
  },
});


To read the value of todoListStatsState, we use useRecoilValue() once again:


TodoListStatSstateの値を読むには、userecoilValue()をもう一度使用します。

const TodoListStats: React.VFC = () => {
  const { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted } =
    useRecoilValue(todoListStatsState);

  const formattedPercentCompleted = Math.round(percentCompleted);

  return (
    <ul>
      <li>Total items: {totalNum}</li>
      <li>Items completed: {totalCompletedNum}</li>
      <li>Items not completed: {totalUncompletedNum}</li>
      <li>Percent completed: {formattedPercentCompleted}</li>
    </ul>
  );
};

以上までの実装を完了させると、Todoリストは以下のようになります。

f:id:lamechang_dev:20220205181605p:plain

ここまでの実装はまさにRecoilのコアコンセプトといったところでしょうか。Reduxなどと比べるとそもそものコード記述量も少なく、かつ簡潔に記述できているな、といったのが率直な印象です。

さて、次回は Asynchronous Data Queries について書こうかと思います。

【TypeScript】【React】【Recoil】翻訳しながらそれなりに型をつけて細々と進めていくRecoil Tutorial その1 Atom編

f:id:lamechang_dev:20220213113950p:plain

本記事は、Reactの提供元であるFacebook改めMetaが開発中の新しい状態管理ライブラリである Recoil の公式チュートリアルを翻訳しつつ、TypeScriptで型付けをしながら進めていく連載の第一弾です。細々とやっていきます🙄 まずは Atom について。

recoiljs.org

Atoms

Atoms contain the source of truth for our application state. In our todo-list, the source of truth will be an array of objects, with each object representing a todo item.

We'll call our list atom todoListState and create it using the atom() function:


Atomsは私たちのアプリケーションの状態のための信頼すべきソースを含みます。私たちのTODOリストでは、信頼すべきソースはオブジェクトの配列になり、各オブジェクトはTodo項目を表します。

atom() 関数を利用して、リストのAtom todoListStateを作成しましょう。

少し後で明らかになりますが、このTodoオブジェクトの型が分かるのでちゃんとジェネリクスを使ってAtomの型を明らかにしてあげましょう。

type Todo = {
  id: number;
  text: string;
  isComplete: boolean;
};

const todoListState = atom<Array<Todo>>({
  key: "todoListState",
  default: [],
});

 

We give our atom a unique key and set the default value to an empty array. To read the contents of this atom, we can use the useRecoilValue() hook in our TodoList component:

The commented-out components will be implemented in the sections that follow.


アトムに一意のキーを与え、デフォルト値を空の配列に設定します。このアトムの内容を読み取るには、TodoListコンポーネントでuseRecoilValue()フックを使用できます。

コメントアウトされたコンポーネントは、以下のセクションで実装されます。

実際の例では function() 記法で書かれていますが、あまり主流でないのでArrow Functionで書いてあげていいと思います。 ちなみに、先ほど todoListStateに型をつけたので、以下のtodoList には Todo[] の型がついていることがIDEから確認できるはずです。

const TodoList: React.VFC = () => {
  const todoList = useRecoilValue(todoListState);

  return (
    <>
      <TodoItemCreator />
      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
};

 

To create new todo items, we need to access a setter function that will update the contents of the todoListState. We can use the useSetRecoilState() hook to get a setter function in our TodoItemCreator component:


新しいToDoアイテムを作成するには、ToDoListateの内容を更新するセッター関数にアクセスする必要があります。 ToDoItemCreatorコンポーネントでsetter機能を取得するには、useSetRecoilState() フックを使用できます。

以下のコードのsetTodoListの型は SetterOrUpdater<Todo[]> というものになり、SetterOrUpdaterの型は以下の通りです。現在の値を受け取るCallbackも渡せるし、純粋に値を渡すこともできるということですね。useState()にそっくりですね😮

export type SetterOrUpdater<T> = (valOrUpdater: ((currVal: T) => T) | T) => void;`
const TodoItemCreator: React.VFC = () => {
  const [inputValue, setInputValue] = useState("");
  // setTodoList: SetterOrUpdater<Todo[]> 
  const setTodoList = useSetRecoilState(todoListState);

  const addItem = () => {
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false,
      },
    ]);
    setInputValue("");
  };

  const onChange = ({ target: { value } }: { target: { value: string } }) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
};

// utility for creating unique Id
let id = 0;
function getId() {
  return id++;
}

 

Notice we use the updater form of the setter function so that we can create a new todo list based on the old todo list.

The TodoItem component will display the value of the todo item while allowing you to change its text and delete the item. We use useRecoilState() to read todoListState and to get a setter function that we use to update the item text, mark it as completed, and delete it:


注意:古いTODOリストに基づいて新しいToDoリストを作成できるように、セッター機能のアップデータ形式を使用します。

TodoItemコンポーネントはTODO項目の値を表示し、テキストを変更してアイテムを削除することを可能にします。 userecoilState()を使用してTodolistStateを読むことと、アイテムテキストの更新(テキストの更新・完了のマーク・または削除)に使用するセッター関数を取得します。

const TodoItem: React.VFC<{ item: Todo }> = ({ item }) => {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const index = todoList.findIndex((listItem) => listItem === item);

  const editItemText = ({
    target: { value },
  }: {
    target: { value: string };
  }) => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      text: value,
    });

    setTodoList(newList);
  };

  const toggleItemCompletion = () => {
    const newList = replaceItemAtIndex(todoList, index, {
      ...item,
      isComplete: !item.isComplete,
    });

    setTodoList(newList);
  };

  const deleteItem = () => {
    const newList = removeItemAtIndex(todoList, index);

    setTodoList(newList);
  };

  return (
    <div>
      <input type="text" value={item.text} onChange={editItemText} />
      <input
        type="checkbox"
        checked={item.isComplete}
        onChange={toggleItemCompletion}
      />
      <button onClick={deleteItem}>X</button>
    </div>
  );
};

const replaceItemAtIndex = (arr: Todo[], index: number, newValue: Todo) => {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
};

const removeItemAtIndex = (arr: Todo[], index: number) => {
  return [...arr.slice(0, index), ...arr.slice(index + 1)];
};

 

And with that we've got a fully functional todo list! In the next section we'll see how we can use selectors to take our list to the next level.


これで、完全に機能するToDoリストができました。次のセクションでは、セレクターを使用してリストを次のレベルに引き上げる方法を説明します。

と、いうことでここまでが完了すると以下のようなシンプルなTodoリストが出来上がります。次は Selectorの部分について書きます。

f:id:lamechang_dev:20220205121634p:plain

【JavaScript/TypeScript】条件に応じてオブジェクトのプロパティを追加する方法

※ ES6以上の動作環境

JavaScriptにおいて、条件に応じて(Conditionallyに)オブジェクトのプロパティを付与する方法について一応書きます。書こうと思った動機は「意外と頻出な実装パターンに見えてあまり日本語の情報にヒットしなかった」から困ってる人いるかも?くらいの気持ちですね😿

javascript conditionally add property to object とかでググれば速攻で答えに辿り着きます。

早速ですが具体例を紹介します。以下のようにオブジェクト taro に対して、特定の条件においてプロパティ wifename を付与したいような状況です。

const isMarried = false;
const taro = {
  age: 25,
  ...(isMarried && { wifename: "hanako" })
};

console.log(taro);

この記法について、今回のようなシンプルな例だと分かりやすくはあるんですが、実際の現場ではここまで実装で扱うオブジェクト構造などがシンプルになることなど基本的になく、「場合によっては可読性が低くなるのでで避けるべきではないか?」とちょっと思ったりしました。

が、意外と処理を追って見るとそんなこともない、という結論に至りました。

spread operatorの挙動は原則 Object.assign()と同様であり、上記の記述は以下のように書き直すことができます

const taro = {
  age: 25,
  ...(isMarried && { wifename: "hanako" })
};

// 以下のように表現できる
Object.assign({ age: 25 }, isMarried && { wifename: "hanako" })

この時、 isMarriedがfalseのケースでは以下のような処理になります(これはもちろん論理演算子の挙動)

Object.assign({ age: 25 }, false)

ここでObject.assign()は、すべてのプリミティブ(未定義なども含む)を{}と同じように扱うようなので、空のオブジェクトがマージされたことになり意図した挙動が実現できているといった感じです。大してややこしい話ではないですね。😄

Object.assign()は、すべてのプリミティブ(未定義なども含む)を{}と同じように扱う という挙動を知ってるかどうかかなと思います。

補足

TypeScript(4.5.4で確認)環境だと以下のように booleanをspread operatorで展開しようとする際に以下のようなエラーを吐きます。

const taro = {
  age: 25,
  // Spread types may only be created from object types.ts(2698)
  ...(isMarried && { wifename: "hanako" })
};

こちらについては as による型アサーションを行うのも手かとも思うのですが、やはり予期せぬバグを生みかねないので、素直に三項演算子で書いてあげて良いのではないかと思います。

const taro = {
  age: 25,
  ...(isMarried ? { wifename: "hanako" } : {})
};

【Material UI】スナップショット実行時にコードが修正がないにも関わらず属性値に差分が出る

環境:"@material-ui/core": "^4.10.2”

特定のコンポーネントに一意の属性を自動で付与する挙動の影響でコード上の修正がなくてもスナップショットの実行時に差分が出ることがあります。差分が生じる箇所は aria-xxxxidclass など複数属性のようです。

これが起因して、CI上でのスナップショットテスト実行時に想定外の箇所で差分が出ることでCIがコケる、なんてことも起こり得ます😿結構広く使われているライブラリなので、変数を操作するための設定値などを持たせてあげられると便利ですが、現状そうでもないらしく・・・

<button
-                  aria-controls="mui-p-100000-P-individual"
+                  aria-controls="mui-p-53434-P-individual"
                   aria-selected="false"
                   class="MuiButtonBase-root MuiTab-root MuiTab-textColorInherit makeStyles-tab"
-                  id="mui-p-100000-T-individual"
+                  id="mui-p-53434-T-individual"
                   role="tab"
                   tabindex="-1"
                   type="button"

対処法

https://github.com/mui-org/material-ui/issues/21293

ここで議論されている内容では、以下のように正規表現による属性値を置換をする方法があげられていますが、これはなかなか良さそうです。が、属性値ごとに適切な正規表現を用意して記述が必要な場合もあるだろうし、少しめんどくさそうですね。

expect(wrapper.html().replace(/id="mui-[0-9]*"/g, '').replace(/aria-labelledby="(mui-[0-9]* *)*"/g, ''))
  .toMatchSnapshot();

もう1つがMath.random()の値をmockして不変にする方法ですね。以下のように jestの spyOn() を利用することで、Math.random()の返り値を不変にすることで差分発生を回避できます。こちらはシンプルな方法ですね。

const mathRandomMock = jest.spyOn(Math, "random");
    mathRandomMock.mockReturnValue(1);

Jestの toMatchSnapShot は、ローカル実行において・CIにおいては少し処理に時間がかかる、という点もありますが出力結果の差分として非常に明瞭なものですし、安全にフロントエンドを開発する際には非常に重宝しますね。

TailWindCSSをスタイリングに採用すると、クラス名の変更がそのままスタイルの変更にも跳ねるので、スナップショットテストの有用性がより上がるのではないかな、と思います。

【material-ui】Gridコンポーネントのブレークポイント propsにbooleanを渡した際の挙動を知る

material-uiのgridコンポーネントとprops

material-uiをデザインシステムとして利用した際に、レスポンシブデザインの対応方法としてgridレイアウトにブレークポイントを設定することは多いと思います。以下のような設定値を渡すことができます。

xs 全てのサイズに対応
sm(small): 600dp〜
md(medium): 960dp〜
lg(large): 1280dp〜
xl(xlarge): 1920dp〜

bootstrapなどの経験があれば見慣れた定義だとは思います(実はブレイクポイントの値が違います)。モバイルファーストで考えてレイアウトしていき、必要に応じてmd、lg等を追加して大きいサイズでのスクリーンを考慮した実装を進めることになりそうです。

公式docの grid コンポーネント を見れば、container item propsの使い方やspacing, そしてbreakpointを設定した際の挙動があらかた追えるのではないでしょうか。

docのサンプルには、codesandboxへのリンクが実はついているので、コードをいじって挙動を確かめたい時に非常に便利です。自分もこれで諸々実験をすることでGridレイアウトの勉強をしました。

f:id:lamechang_dev:20210615190011p:plain

Grid コンポーネントのコードサンプルに登場する、booleanで定義されたブレークポイント

複雑なGrid という例に登場する、Gridが入れ子になっており多少複雑なレイアウトのコードを読んでいる際に、ブレークポイントのpropsに対してbooleanを指定する記述がを見つけました。以下の通り。 f:id:lamechang_dev:20210615190431p:plain

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
import Paper from '@material-ui/core/Paper';
import Typography from '@material-ui/core/Typography';
import ButtonBase from '@material-ui/core/ButtonBase';

const useStyles = makeStyles((theme) => ({
  root: {
    flexGrow: 1,
  },
  paper: {
    padding: theme.spacing(2),
    margin: 'auto',
    maxWidth: 500,
  },
  image: {
    width: 128,
    height: 128,
  },
  img: {
    margin: 'auto',
    display: 'block',
    maxWidth: '100%',
    maxHeight: '100%',
  },
}));

export default function ComplexGrid() {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <Paper className={classes.paper}>
        <Grid container spacing={2}>
          <Grid item>
            <ButtonBase className={classes.image}>
              <img className={classes.img} alt="complex" src="/static/images/grid/complex.jpg" />
            </ButtonBase>
          </Grid>
          <Grid item xs={12} sm container>
            <Grid item xs container direction="column" spacing={2}>
              <Grid item xs>
                <Typography gutterBottom variant="subtitle1">
                  Standard license
                </Typography>
                <Typography variant="body2" gutterBottom>
                  Full resolution 1920x1080 • JPEG
                </Typography>
                <Typography variant="body2" color="textSecondary">
                  ID: 1030114
                </Typography>
              </Grid>
              <Grid item>
                <Typography variant="body2" style={{ cursor: 'pointer' }}>
                  Remove
                </Typography>
              </Grid>
            </Grid>
            <Grid item>
              <Typography variant="subtitle1">$19.00</Typography>
            </Grid>
          </Grid>
        </Grid>
      </Paper>
    </div>
  );
}

これ、例えばsm = {6}のように指定すると、表示幅がsm(600px)を超えると6列分の範囲を占めるようにレイアウトされるみたいな挙動については理解していましたが、上記のコードにもある通り、値としてboolean値を渡した際の挙動がイマイチわかりません。

gridのapi仕様 を見ると確かにブレークポイントにbooleanを渡せることは記載がありますが、その挙動については特に言及されていません。なのでコードを読んでみました。

  GRID_SIZES.forEach((size) => {
    const key = `grid-${breakpoint}-${size}`;

    if (size === true) {
      // For the auto layouting
      styles[key] = {
        flexBasis: 0,
        flexGrow: 1,
        maxWidth: '100%',
      };
      return;
    }

    if (size === 'auto') {
      styles[key] = {
        flexBasis: 'auto',
        flexGrow: 0,
        maxWidth: 'none',
      };
      return;
    }

    // Keep 7 significant numbers.
    const width = `${Math.round((size / 12) * 10e7) / 10e5}%`;

    // Close to the bootstrap implementation:
    // https://github.com/twbs/bootstrap/blob/8fccaa2439e97ec72a4b7dc42ccc1f649790adb0/scss/mixins/_grid.scss#L41
    styles[key] = {
      flexBasis: width,
      flexGrow: 0,
      maxWidth: width,
    };
  });

どうやら、sizetrueであった場合は、以下のプロパティが適用されるみたいですね。表示幅が指定した幅を超えると、余ったスペースをflexGrow記載の比率で伸びるようなレイアウトになる、と言うことです。上記の複雑なGridコンポーネントの例では、カードの文字部分について、幅がsm以下に相当する場合は画像に対して1つ下のグリッド行に移動し、またsm以上になったタイミングで画像と同じ行において、横並びに余ったスペースを埋め尽くすようなレイアウトになっていることがわかりました。

flexBasis: 0,
flexGrow: 1,
maxWidth: '100%',

また、falseが適用された場合は何も適用されないようです。