$shibayu36->blog;

クラスター株式会社のソフトウェアエンジニアです。エンジニアリングや読書などについて書いています。

我が家のイヤイヤ期の子供の着替えテクニック

最近は息子がいわゆる「イヤイヤ期」に入っていて、とにかく全てのことに嫌というようになった。例えば自転車に乗って帰るのも嫌だし、お風呂場から帰ってきた後着替えるのも嫌で裸で走り回るし、起きた後着替えるのも嫌で地面でバタバタしている。

それでもなんとか手早く終わらせないといけないので、子育てに関する情報を参考にし、さらに創意工夫をして我が家の中ではどちらかというとうまくいくテクニックを見つけていっている。その中で今回は我が家の着替えテクニックについて書いてみる。


着替えテクニックでいくつか見つけたのは

  • 自分が着ようとする or 人形に着せようとする
  • 頭を働かせるような別のことに集中させる
  • 2択で自分で選ばせる
  • 気分を変えて別の人にバトンタッチ

自分が着ようとする or 人形に着せようとする

我が家で最近一番うまくいっているテクニックは、自分が着ようとする or 人形に着せようとするというもの。

服を着せようとして嫌と言われたら、「じゃあお父さんが着るねー」「この人形さんに着せてあげよー」って言って着せる真似をする。そうすると「着せるの嫌や!」って怒り出す。そこですかさず「じゃあ自分で着る?」って聞くことで、着てくれるようになった。

最近の息子は所有欲も強いので、服は自分のものという意識が強く、着られるくらいなら自分が着てやるという気持ちになるのかな?と思った。

頭を働かせるような別のことに集中させる

我が家でそこそこ上手く行っているテクニックとしては、頭を目一杯働かせるような別のことに集中させること。

とにかく別のことに頭を働かせることで、「服を着る」ことを考える頭をなくさせる作戦。具体的には息子は今は乗り物にハマっているので、乗り物の図鑑を見せて、この車は何?この車は何?消防車はどこ?などと矢継ぎ早に質問を投げかけていき、それと同時に着替えさせている。なぜか質問を投げかけている時は着替えるために素直に手や足を上げてくれたりするので面白い。

ちなみにできる限り頭を使わせるタスクをやらせないとうまく行かなかった。例えば「動画を見せる」だと頭をそこまで使っていないらしく、嫌やという率が高く感じる。動画見てる時ってそんなに頭使ってないんだな...

2択で自分で選ばせる

我が家で2~3割くらいでうまくいくテクニックとしては、自分で着る服を選ばせること。

例えばTシャツを2つピックアップして、「どっちを着る?」と聞く。こっち!と言われたら、じゃあ着ようか〜といって着替えさせる。

このときのポイントは、2つをピックアップすること。大量の服から選ばせようとすると楽しくなってしまうらしく遊び始めてしまう。2つだけの中から「どっち?」と聞くと、選ぶことが多くなった。

気分を変えて別の人にバトンタッチ

これは最悪ケースのときに使ってる...着替えを失敗し続けると、着替え自体じゃなくて、着替えさせようとしている人が嫌になってくるようで、とにかくキレてくる。そういう時は諦めて妻にバトンタッチしている...

まとめ

今回は我が家のイヤイヤ期の子供の着替えテクニックについて書いてみた。あくまでも我が家で確率が高いだけで、子供によって確率の高いテクニックはぜんぜん違うので、参考程度になれば嬉しい。

しかし子供を持つ家庭はいろいろ大変な思いをして育児してますね...これは育休を取らなかったら表面的にしか理解できなかったと思う。育休を長期間取れば大変さを身を持って体験でき、本質的に理解できるので良いですね。

TypeScript + Apollo ClientでGraphQLのデータに型を付ける

TypeScript + Apollo Clientで、useQueryなどを用いてGraphQLのクエリを発行する際に、クエリのvariablesやレスポンスのデータに型を付けたい。やり方が少々分かりづらかったのでメモを残す。

型をつけるためにやることは以下の通り。

  • apollo.config.jsを定義する
  • graphql-tagでGraphQLのクエリを書く
  • apollo client:codegenで型情報を生成する
  • クエリを書いた場所で型をimportし、useQueryの型パラメータに指定する

shibayu36/go-Intern-Diaryのレポジトリで試している。

apollo.config.jsを定義する

まずApolloのコードジェネレータが適切に動作するために、apollo.config.jsを作る。Configuring Apollo projects - Apollo Basics - Apollo GraphQL Docsを参考にする。

今回の場合、GraphQLのスキーマはGraphQLエンドポイントから取得し、クライアントから発行するクエリは./ui/src/以下のファイルに定義するので、次の内容になる。

module.exports = {
  client: {
    includes: ['./ui/src/**/*.tsx', './ui/src/**/*.ts'],
    service: {
      name: "go-Intern-Diary",
      url: "http://localhost:8000/query"
    }
  }
};

graphql-tagでGraphQLのクエリを書く

続いてクライアント側でgraphql-tagを使ってクエリを書く。クエリに付けた名前が型情報の名称に使われるため、必ずクエリにプロジェクトでユニークな名前を付けておく。

ui/src/pages/diary.tsx

import gql from "graphql-tag";

...

const getDiaryQuery = gql`
  query GetDiary($diaryId: ID!) {
    diary(diaryId: $diaryId) {
      id
      name
    }
  }
`

apollo client:codegenで型情報を生成する

apollo.config.jsを用意し、graphql-tagでクエリを書いていれば、apollo client:codegenコマンド一つで型情報を生成できる。useReadonlyTypesオプションを使うと全てreadonlyで型情報が作成されるため、意図しない代入を防げる。好みに合わせて--outputFlatも使うと良さそう。

npx apollo client:codegen --useReadOnlyTypes --target=typescript

これでui/src/pages/__generated__/GetDiary.tsのように、クエリ名がファイル名となり型情報が出力される。

クエリを書いた場所で型をimportし、useQueryの型パラメータに指定する

あとは上で出力された型情報をimportし、useQueryの型パラメータに指定する。

ui/src/pages/diary.tsx

import { GetDiary, GetDiaryVariables } from "./__generated__/GetDiary";

...

const { loading, error, data } = useQuery<GetDiary, GetDiaryVariables>(
  getDiaryQuery,
  {
    variables: { diaryId: diaryId }
  }
);

これでdata変数はGetDiary型となり、変なプロパティにアクセスするとコンパイルエラーが発生するようになる。またvariablesもdiaryId以外のプロパティを指定するとコンパイルエラーになる。間違った使い方にすぐに気付けるようになって安心。

ちなみに、useQueryはloadingやerrorが返ってくる時dataはundefinedとなるため、data.diaryとかするとObject is possibly 'undefined'のようにコンパイルエラーになってしまう。先にloadingやerrorのチェックをしていれば、dataは存在するとみなせるはずなので、!.演算子を使うと良さそう(参考: tslint - In Typescript, what is the ! (exclamation mark / bang) operator when dereferencing a member? - Stack Overflow)。

ui/src/pages/diary.tsx

if (loading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;

return <div className="Diary">
  <h1>{data!.diary.name}</h1>
</div>;

参考: variablesが存在しない時のやり方

クエリにvariablesが存在しない時はuseQueryの型パラメータの一番目のみ指定すれば良い。

ui/src/pages/diaries.tsx

const { loading, error, data } = useQuery<GetMyDiaries>(getMyDiariesQuery);

まとめ

useQueryの型の付け方や、client:codegenの使い方がドキュメントからだとよく分からずに結構ハマってしまった。今回で理解できたので、ようやくGraphQLのクエリをバリバリ書けそう。

Apollo platformのチュートリアルをやった

Hatena-Textbook 2018学習日記(5) - GraphQL編 - $shibayu36->blog;のようにHatena-Textbookを用いて最近のモダンなWebアプリケーション開発の学習をしているのだけど、TypeScript + GraphQL + Apollo Client + Reactの部分でそれぞれの技術の基本知識を理解できていなかったので、エラーが起きたときに何から直したらよいかわからない状態になってしまっていた。

前回はGraphQLのクエリについて学んだ - $shibayu36->blog;でGraphQLを少し掘り下げたので、今回はApollo platformについてチュートリアル( https://www.apollographql.com/docs/tutorial/introduction/ )を行いながら学習を進めていった。

Apollo platformとは

JSからGraphQLを使うときにめっちゃ便利に使えるフレームワーク。これだけでGraphQLサーバも書けるし、クライアントからGraphQLサーバへのアクセスも書ける。

特に面白いなと思ったのは、サーバサイドのGraphQLスキーマを、クライアントサイドで拡張して、GraphQLサーバのデータとクライアントサイドのデータを透過的に扱えるところ。これによって状態の取り扱いが非常にやりやすくなりそう。世のフロントエンドエンジニアにApollo Clientを布教したい - Qiitaが分かりやすいので参照のこと。

学習メモ

あとは学習メモ。今はApollo Clientの方を特に学習したかったので、GraphQLのサーバを作る部分はほとんどメモはない。

4. Run your graph in production - Apollo Basics - Apollo GraphQL Docs

  • Graph ManagerというGraphQLのSchemaをアップロードできるクラウドサービスがある

5. Connect your API to a client - Apollo Basics - Apollo GraphQL Docs

  • Apollo Clientを使うことでいい感じにキャッシュをしてくれる
  • またlocalでもremoteでも透過的に扱うことができる

6. Fetch data with queries - Apollo Basics - Apollo GraphQL Docs

  • useQueryはdata, loading, errorを使う形式が最もよく使う。const { data, loading, error } = useQuery(GET_LAUNCHES);
  • useQueryではfetchMoreを取ってきて、ページングすることができる。fetchMoreにわたすupdateQueryの返す値は、これまでのデータをmergeしたものを返す必要がある
        fetchMore({
          variables: {
            after: data.launches.cursor,
          },
          updateQuery: (prev, { fetchMoreResult, …rest }) => {
            if (!fetchMoreResult) return prev;
            return {
              …fetchMoreResult,
              launches: {
                …fetchMoreResult.launches,
                launches: [
                  …prev.launches.launches,
                  …fetchMoreResult.launches.launches,
                ],
              },
            };
          },
        })
  • useQueryにはvariablesを渡せる
  const { data, loading, error } = useQuery(
    GET_LAUNCH_DETAILS,
    { variables: { launchId } }
  );
  • fetchPolicyを用いれば、キャッシュから先に取得するかなどの条件を変更できる。network-onlyを使えば毎回APIから取得してくれる
  const { data, loading, error } = useQuery(
    GET_MY_TRIPS,
    { fetchPolicy: "network-only" }
  );

7. Update data with mutations - Apollo Basics - Apollo GraphQL Docs

  • mutationの処理にはuseMutationを使う。useMutationを使うとmutateのための関数が返ってくるので、それをコンポーネントに引き渡し、利用すると良い
  • また、ログイン処理で返ってきた値をlocalStorageに保存するなどの処理は、onCompletedを使う。
  • Apollo Clientを直接参照したいときはuseApolloClientを使う。これをすることでclientに保持されているデータにアクセスできる
export default function Login() {
  const client = useApolloClient();
  const [login, { loading, error }] = useMutation(
    LOGIN_USER,
    {
      onCompleted({ login }) {
        localStorage.setItem('token', login);
        client.writeData({ data: { isLoggedIn: true } });
      }
    }
  );

  if (loading) return <Loading />;
  if (error) return <p>An error occurred</p>;

  return <LoginForm login={login} />;
}
  • Apollo Clientがリクエストするときに送信するヘッダはnew ApolloClientのときに指定できる。これを使えばログイントークンをAuthorizationヘッダに含めるなどのことができる。
const client = new ApolloClient({
  cache,
  link: new HttpLink({
    uri: 'http://localhost:4000/graphql',
    headers: {
      authorization: localStorage.getItem('token'),
    },
  }),
});

8. Manage local state - Apollo Basics - Apollo GraphQL Docs

  • 内部状態はreduxなど別のものを使わずApollo cacheに統一しておくのが良い
  • 内部状態の取得はスキーマを定義することでGraphQLと同じような形で取得できる。
  • extendを使ってサーバーのスキーマを拡張できる
export const typeDefs = gql`
  extend type Query {
    isLoggedIn: Boolean!
    cartItems: [ID!]!
  }

  extend type Launch {
    isInCart: Boolean!
  }

  extend type Mutation {
    addOrRemoveFromCart(id: ID!): [Launch]
  }
`;
// ↑をnew Apollo Clientにわたす
  • クライアント側のスキーマを使うときは、@client directiveを使うことでアクセスできる。cacheのdataに入っているものは特にclient側のresolversを定義せずとも取得できる?以下のようにしていればclientサイドで作ったisLoggedInやcartItemsは取得できそう
cache.writeData({
  data: {
    isLoggedIn: !!localStorage.getItem(‘token’),
    cartItems: [],
  },
});
  • ↑を取得するためのクエリは@client directiveを使ってこう書く
  query IsUserLoggedIn {
    isLoggedIn @client
  }
  query GetCartItems {
    cartItems @client
  }
  • クライアントサイドでresolverを定義すれば、サーバのGraphQLスキーマに対し、Virtual Fieldを付与することもできる。以下はサーバ側のLaunch typeに対し、isInCartフィールドを追加する例。
export const resolvers = {
  Launch: {
    isInCart: (launch, _, { cache }) => {
      const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
      return cartItems.includes(launch.id);
    },
  },
};
  • useMutationのrefetchQueriesを使うことで、更新後のデータの再取得ができる
  • localデータのmutationを定義するときは、cache.writeQueryを使うと便利
export const resolvers = {
  Mutation: {
    addOrRemoveFromCart: (_, { id }, { cache }) => {
      const { cartItems } = cache.readQuery({ query: GET_CART_ITEMS });
      const data = {
        cartItems: cartItems.includes(id)
          ? cartItems.filter(i => i !== id)
          : […cartItems, id],
      };
      cache.writeQuery({ query: GET_CART_ITEMS, data });
      return data.cartItems;
    },
  },
};