20210125のReactに関する記事は9件です。

[React Native] サーバーやDBからのデータ取得

0. 目的

Hooksを用いてサーバーやDBからデータを取得

1. 環境

  • React : 16.8.6
  • React Native : 0.63.4

2. ソースコード

GetData.js
import React from 'react';
import {FlatList, Text, View} from 'react-native';

import {useEffect, useState} from 'react';

function App() {
  const [response, setResponse] = useState([]);

  useEffect(() => {
    // データ取得(DBの場合もここでread処理を行う)
    fetch('https://zipcloud.ibsnet.co.jp/api/search?zipcode=7830060') // [1]
      .then((response) => response.json())
      .then((responseJson) => {
        setResponse(responseJson['results']);
      });
  }, []);

  if (!response.length) { // データ取得前
    return <Text>Loading...</Text>;
  } else if (response.length) { // データ取得後
    return (
        <FlatList
          data={response}
          renderItem={({item}) => (
            <View>
              <Text>{item.zipcode}</Text>
              <Text>
                {item.address1}
                {item.address2}
                {item.address3}
              </Text>
            </View>
          )}
          keyExtractor={(item) => item.zipcode}
        />
    );
  }
}

export default App;

3. 結果

2

「Loading」が表示された後、上の画像のように描画される.

4. 解説

処理の流れ
1. 「Loading」を表示
2. 「useEffect」内の処理を実行
3. 「住所」を表示

useEffectはレンダリング後に実行されるため、responseの有無により描画内容を変更する.
また、setResponseによりresponseの値が変更された際に再レンダリングされるため、データ取得後には再度if文が実行され、データ内容が表示される.

5. 参考

[1]郵便番号検索API

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ReactのConcurrent Modeでインターフェースプレビューを実装してみた

はじめに

都内の企業でモバイルアプリ開発を行っているすずきと申します。早いもので、エンジニアになって半年経ちました。

最近、開発アプリのある画面でAPIから最新のデータを都度取得するような実装を行いました。データ取得中の画面として以下のようなインジケータを追加したのですが、テストで何度か挙動を確認したところ、画面遷移のたびに表示されて軽くストレスを感じました。

UXの質が落ちてるな...と感じたので、YouTubeアプリみたいな感じで画面遷移中も前の画面を残す(インターフェースプレビュー)ようにできないかなぁと調べたのですが、Reactの性質上、そのような実装は難しいということがわかりました。

Reactにも弱点はあるんだな...と思いながらReactの公式ドキュメントを眺めていたところ、Concurrent Modeというページをふとみつけました。

スクリーンショット 2021-01-24 15.35.20.png image.png

Concurrent Mode(並列モード)

公式Docsを読みすすめてみると、どうやらConcurrent Modeを使うとReactで中断可能なレンダリングが可能になるとのことでした。

更新のレンダーを一度始めたら中断することはできませんよ!というのが、ReactのようなUIライブラリの動作の基本だったのですが、Concurrent Modeだと中断できるようになったということですね。

前の画面を描画させながら、更新後の画面のレンダーをメモリ内で継続し、データが到着するごとに再レンダーを行うことができるので、この仕組みを利用することで、前の画面を表示したまま、インジケータをインライン表示するよう React に伝えることができます。新しい画面の準備が完了したら、React が自動的にそちらを表示します。

SuspenseとuseTransition

Concurrent Modeでインターフェースプレビューを実現するためにキーワードとなるのが、SuspenseとuseTransitionです。

Suspense

SuspenseはReact 16.6で追加されたコンポーネントです。コンポーネントが読みだそうとしているデータが準備できていないとき、宣言的にロード中の状態を指定することができるようになります。

Suspenseを使用する目的の1つに、RESTやGraphQLのデータ取得ライブラリとReactとの深い連携があります。データ取得ライブラリがSuspenseをサポートすることで、コンポーネントからそれを自然に扱えるようになります。Suspenseは連携するライブラリの種類を選びませんが、Facebookで本番環境として利用しているライブラリはRelayのみだそうです(Apolloでは今のところテストされていないようです...)。

useTransition

新しい画面に遷移する前に新しいコンテンツがロードされるのを待機するために使用するのがReact Hooksの1つであるuseTransitionです。

const [startTransition, isPending] = useTransition({
  timeoutMs: 3000
});

startTransitionでどのstateの更新を遅延させたいのかをReactに伝え、isPendingでトランジションが進行中かどうかを伝えます。

timeoutMsプロパティでトランジションが終了するまでどれだけ待てるかを指定します。例えば、{timeoutMs: 3000}で、「新しい画面がロードされるのに3秒以上かかったら大きなスピナーを表示せよ、それまでは前の画面を表示しつづけて構わない」ということを伝えています。

データ取得アプローチの比較

インターフェースプレビューの実装が今回の目的なのですが、勉強のためにReactのデータ取得の各種アプローチについて検証してみることにしました。公式Docsの例をベースに、Material-UIの勉強も兼ねて新しく実装しました。

プロフィール部分とグラフ部分がどのような順番で表示されるのかどうかを検証します。ソースはGitHubにあります。

スクリーンショット 2021-01-24 21.33.01.png

1. Fetch-on-Render(Suspense不使用)

ezgif.com-gif-maker.gif

Reactアプリケーションのデータ取得は、classコンポーネントであればライフサイクルメソッド(component~)、functionalコンポーネントであれば副作用(effect)を用いるのが一般的です。


Fetch-on-Renderというのは、このようなレンダー後にデータ取得が始まるようなアプローチのことをいいます。

FetchOnRender.jsx
export const FetchOnRender = () => {
  const [userData, setUserData] = useState(null);

  useEffect(() => { // ①, ③
    fetchUserData().then((u) => setUserData(u));
  }, []);

  if (userData === null) { // ②
    return (
      <Typography variant="p" component="h7">
        Loading profile...
      </Typography>
    );
  }
  return (
    <div>
      <DetailsContent
        company={userData.data.company}
        name={userData.data.name}
        image={userData.data.image}
      />
      <ProfileChart />
    </div>
  );
};

const ProfileChart = () => {
  const [chartData, setChartData] = useState(null);

  useEffect(() => { // ④, ⑥
    fetchChartData().then((p) => setChartData(p));
  }, []);

  if (chartData === null) { // ⑤
    return (
      <Typography variant="p" component="h7">
        Loading chart...
      </Typography>
    );
  }

  return <ChartContent data={chartData} />;
};

コードの実行順は以下のようになります。

①プロフィールデータ取得開始->②待機->③プロフィールデータ取得完了->④グラフデータ取得開始->⑤待機->⑥グラフデータ取得完了

グラフデータの取得はプロフィールデータを取得するまで開始されません。これをウォーターフォールといいます。

2. Fetch-Then-Render(Suspense未使用)

ezgif.com-gif-maker (1).gif

Fetch-Then-Renderは、Fecth-on−Renderで起こったウォーターフォールを防止するために、データ取得を1つの副作用で行うアプローチのことをいいます。


FetchThenRender.jsx
const promise = fetchProfileData();

export const FetchThenRender = () => {
  const [userData, setUserData] = useState(null);
  const [chartData, setChartData] = useState(null);

  useEffect(() => { // ①, ②, ③, ④, ⑤
    promise.then((data) => {
      setUserData(data.userData);
      setChartData(data.chartData);
    });
  }, []);

  if (userData === null) {
    return (
      <Typography variant="p" component="h7">
        Loading profile...
      </Typography>
    );
  }
  return (
    <div>
      <DetailsContent
        company={userData.data.company}
        name={userData.data.name}
        image={userData.data.image}
      />
      <ProfileChart chartData={chartData} />
    </div>
  );
};

const ProfileChart = ({ chartData }) => {
  if (chartData === null) {
    return (
      <Typography variant="p" component="h7">
        Loading chart...
      </Typography>
    );
  }

  return <ChartContent data={chartData} />;
};

コードの実行順は以下のようになります。

①プロフィールデータ取得開始->②グラフデータ取得開始->③待機->④プロフィールデータ取得完了->⑤グラフデータ取得完了

Fetch-on-Renderで発生したウォーターフォールは解決されましたが、ここでも別のウォーターフォールが発生しています。fetchProfileData内部のPromise.all()ですべてのデータが到着するまで待機しているので、グラフデータがロードされるまでプロフィール部分をレンダーすることができません。

noSuspenseApi.js
export const fetchProfileData = () => {
  return Promise.all([fetchUserData(), fetchChartData()]).then(
    ([userData, chartData]) => {
      return { userData, chartData };
    }
  );
};

3. Render-as-You-Fetch(Suspense使用)

ezgif.com-gif-maker (2).gif

Fetch-on-RenderやFetch-then-RenderのようなSuspenseを使用しないアプローチでは、データ取得完了->レンダー開始という順番でしたが、Suspenseを使うと、このステップが入れ替わり、レンダー開始->データ取得完了という順番になります。


RenderAsYouFetch.jsx
const resource = fetchProfileData();

export const RenderAsYouFetch = () => {
  return <ProfilePage />;
};

const ProfilePage = () => {
  return (
    <Suspense
      fallback={
        <Typography variant="p" component="h7">
          Loading profile...
        </Typography>
      }
    >
      <ProfileDetails />
      <Suspense
        fallback={
          <Typography variant="p" component="h7">
            Loading chart...
          </Typography>
        }
      >
        <ProfileChart />
      </Suspense>
    </Suspense>
  );
};

const ProfileDetails = () => {
  const userData = resource.userData.read();
  return (
    <DetailsContent
      company={userData.data.company}
      name={userData.data.name}
      image={userData.data.image}
    />
  );
};

const ProfileChart = () => {
  const data = resource.chartData.read();
  return <ChartContent data={data} />;
};

以上のコードでは、レンダー時点でfetchProfileData()をつかってリクエストがスタートしています。今回の検証ではフェイクのAPI実装を行っていますが、実際はRelayやApolloのようなデータ取得ライブラリのSuspense連携機能を使います(上述していますが、連携機能がテストされているのは今のところRelayのみです)。

RenderAsYouFetchのレンダーを試みます。子要素として、ProfileDetailsとProfileChartが返ります。

ProfileDetailsのレンダーを試みます。内部でresource.userData.read()が呼び出されます。データはまだ取得されていないので、このコンポーネントはSuspendし、ツリーの他のコンポーネントのレンダーを試みます。

ProfileChartのレンダーを試みます。内部でresource.chartData.read()が呼び出されます。こちらのデータもまだ取得されていないので、このコンポーネントはSuspendします。Reactはこのコンポーネントも飛ばして、ツリーの他のコンポーネントのレンダーを試みます。

レンダーを試みる他のコンポーネントは残っていないので、ProfileDetails直上のSuspendのfallbackを表示します。

fallback={
  <Typography variant="p" component="h7">
    Loading chart...
  </Typography>
}

このように、データが到着するごとに再レンダーを試み、そのたびに深いところまで表示できるようになります。Fetch-Then-Renderでも存在していたウォーターフォールの問題がありません。

Fetch-Then-Renderのようにプロフィールとグラフを同時に出現させたい場合は、ProfileChartをラップしているSuspenseを取り除けば可能です。

    <Suspense
      fallback={
        <Typography variant="p" component="h7">
          Loading profile...
        </Typography>
      }
    >
      <ProfileDetails />
       <ProfileChart />
    </Suspense>

4. Race Condition

ezgif.com-gif-maker (3).gif

Suspenseによるインターフェースプレビューの実装の前に、useEffectやcomponentDidMountでデータ取得を行った場合に引き起こされるRace condition(競合状態)についても検証しました。


ContentionalStateコンポーネントをつくり、ProfilePageと複数のプロフィールを切り替えるボタンを配置しました。

const getNextId = (id) => {
  return id === 3 ? 0 : id + 1;
};

export const ContentionState = () => {
  const [id, setId] = useState(0);
  return (
    <div>
      <Button
        variant="outlined"
        color="primary"
        onClick={() => setId(getNextId(id))}
      >
        次へ
      </Button>
      <ProfilePage id={id} />
    </div>
  );
};

ProfilePageとProfileChartの両方にuseEffectがあり、idが変わるたびにそれぞれのデータを取得します。"次へ"ボタンを素早く押すと、プロフィールを別の ID に切り替えた後に以前のプロフィールのリクエストが返ってくることがあります。

React コンポーネントにはライフサイクルがありますが、非同期なリクエストにも同様に、発行したときに始まり、レスポンスを得た時に終わるというライフサイクルをもっています。このような複数のプロセスが互いに影響しあうことで、競合状態が起こります。

ContentionalState.jsx
const getNextId = (id) => {
  return id === 3 ? 0 : id + 1;
};

export const ContentionState = () => {
  const [id, setId] = useState(0);
  return (
    <div>
      <Button
        variant="outlined"
        color="primary"
        onClick={() => setId(getNextId(id))}
      >
        次へ
      </Button>
      <ProfilePage id={id} />
    </div>
  );
};

const ProfilePage = ({ id }) => {
  const [userData, setUserData] = useState(null);

  useEffect(() => {
    fetchUserData(id).then((u) => setUserData(u));
  }, [id]);

  if (userData === null) {
    return (
      <Typography variant="p" component="h7" style={{ marginLeft: 10 }}>
        Loading profile...
      </Typography>
    );
  }
  return (
    <div>
      <DetailsContent
        company={userData.data.company}
        name={userData.data.name}
        image={userData.data.image}
      />
      <ProfileChart id={id} />
    </div>
  );
};

const ProfileChart = ({ id }) => {
  const [chartData, setChartData] = useState(null);

  useEffect(() => {
    fetchChartData(id).then((p) => setChartData(p));
  }, [id]);

  if (chartData === null) {
    return (
      <Typography variant="p" component="h7">
        Loading chart...
      </Typography>
    );
  }

  return <ChartContent data={chartData} />;
};

5. Concurrent Mode

ezgif.com-gif-maker (4).gif

ここからインターフェースプレビューを実装するのですが、まずはRace Conditionを解決する必要があります。


Render-as-You-Fetchの実装ではリソースをトップレベルの変数として定義していましたが、今回は複数リソースを扱うため、ConcurrentModeコンポーネント内で定義します。

const initialResource = fetchProfileData(0);

export const ConcurrentMode = () => {
  const [resource, setResource] = useState(initialResource);

"次へ"ボタンのクリックハンドラで、プロフィールを切り替えるためのstateを設定し、さらにこのstateの更新をuseTransitionのstartTransitionでラップします。これにより、stateの更新で望ましくないローディング中の状態の表示が起きてしまった場合に、Reactがstate更新を遅延させることができます。

      <Button
        variant="outlined"
        color="primary"
        disabled={isPending}
        onClick={() => {
          startTransition(() => {
            const nextUserId = getNextId(resource.userId);
            setResource(fetchProfileData(nextUserId));
          });
        }}
      >
        次へ
      </Button>

さらにuseTransitionのisPendingを使用することで、トランジションの完了を待機しているかどうかを確認することができ、これでようやく前の画面と"Loading..."を同時に表示するというインターフェースプレビュー の状態を実現することができました。

  const [startTransition, isPending] = useTransition({
    timeoutMs: 3000,
  });
ConcurrentMode.jsx
const getNextId = (id) => {
  return id === 3 ? 0 : id + 1;
};

const initialResource = fetchProfileData(0);

export const ConcurrentMode = () => {
  const [resource, setResource] = useState(initialResource);
  const [startTransition, isPending] = useTransition({
    timeoutMs: 3000,
  });

  return (
    <div>
      <Button
        variant="outlined"
        color="primary"
        disabled={isPending}
        onClick={() => {
          startTransition(() => {
            const nextUserId = getNextId(resource.userId);
            setResource(fetchProfileData(nextUserId));
          });
        }}
      >
        次へ
      </Button>
      {isPending ? ' Loading...' : null}
      <ProfilePage resource={resource} />
    </div>
  );
};

const ProfilePage = ({ resource }) => {
  return (
    <Suspense
      fallback={
        <Typography variant="p" component="h7" style={{ marginLeft: 10 }}>
          Loading profile...
        </Typography>
      }
    >
      <ProfileDetails resource={resource} />
      <Suspense
        fallback={
          <Typography variant="p" component="h7">
            Loading chart...
          </Typography>
        }
      >
        <ProfileChart resource={resource} />
      </Suspense>
    </Suspense>
  );
};

const ProfileDetails = ({ resource }) => {
  const userData = resource.userData.read();
  return (
    <DetailsContent
      company={userData.data.company}
      name={userData.data.name}
      image={userData.data.image}
    />
  );
};

const ProfileChart = ({ resource }) => {
  const data = resource.chartData.read();
  return <ChartContent data={data} />;
};

おわりに

公式Docsの説明が詳しかったこともあり、自分で実装しながらReactのレンダリングへの理解を深めることができました。Concurrent Modeは実験的機能であり、React Nativeにもまだ対応していません。アプリへの導入はまだ先になるかもしれませんが、正式リリースが今から楽しみです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Next.jsでネストした動的なルーティングの書き方

Next.jsのドキュメント見たけど、自分のコードに落とし込むのが難しいよって人には参考になると思います。

バージョン

  • MacOS Catalina 10.15.7
  • Next.js 10.0.5

今回やりたいこと

以下のような感じで1や10など動的な場合のディレクトリ構成をお伝えできればと思います。
next/linkを使っての説明はなく、あくまでもディレクトリ構成に絞って書いていきます。

  • ユーザー一覧(/users)
  • ユーザー詳細(/users/1)
  • 投稿一覧(/users/1/posts)
  • 投稿詳細(/users/1/posts/10)

先に最終的なディレクトリ構成

pages
├── index.tsx
└── users
    ├── [userId]
    │   ├── index.tsx
    │   └── posts
    │       ├── [postId].tsx
    │       └── index.tsx
    └── index.tsx

やってみる

ユーザー一覧ページの作成

% mkdir src/pages/users && touch src/pages/users/index.tsx
/pages/users/index.tsx
import React from 'react';

const index: React.FC = () => {
  return <h1>/users</h1>;
};

export default index;

スクリーンショット 2021-01-25 18.13.16.png

ユーザー詳細ページの作成

% mkdir src/pages/users/\[userId] && touch src/pages/users/\[userId]/index.tsx
/pages/users/[userId]/index.tsx
import React from 'react';
import { useRouter } from 'next/router';

const index: React.FC = () => {
  const router = useRouter();
  const { userId } = router.query;
  console.log({userId});

  return <h2>/users/: {userId}</h2>;
};

export default index;

/users/1の場合

スクリーンショット 2021-01-25 18.29.23.png

/users/100の場合

スクリーンショット 2021-01-25 18.30.30.png

{userId: ${param}}というqueryが渡ってきてますね!

投稿一覧ページの作成

% mkdir src/pages/users/\[userId]/posts && touch src/pages/users/\[userId]/posts/index.tsx
/src/pages/users/\[userId]/posts/index.tsx
import React from 'react';
import { useRouter } from 'next/router';

const index: React.FC = () => {
  const router = useRouter();
  const { userId } = router.query;
  console.log({userId});

  return <h1>/users/{userId}/posts</h1>;
};

export default index;

スクリーンショット 2021-01-25 18.36.35.png

ちゃんと{userId: "1"}をpostsでも持てていますね!

投稿詳細ページの作成

% mkdir src/pages/users/\[userId]/posts && touch src/pages/users/\[userId]/posts/[postId].tsx
/src/pages/users/\[userId]/posts/[postId].tsx
import React from 'react';
import { useRouter } from 'next/router';

const index: React.FC = () => {
  const router = useRouter();
  const { userId, postId } = router.query;
  console.log({userId, postId});

  return <h1>/users/{userId}/rooms/: {postId}</h1>;
};

export default index;

スクリーンショット 2021-01-25 18.42.03.png

{userId: "1", postId: "1"}が取得できているので良さそうですね!

注意点

const {userId} = router.query;const {postId} = router.queryなどの変数名はディレクトリ名やファイル名と同じにしておかないとうまく取得できないのでご注意ください。

終わりに

参考になりましたら、LGTMほしいです?

上に戻るの面倒な方用にもう一度

pages
├── index.tsx
└── users
    ├── [userId]
    │   ├── index.tsx
    │   └── posts
    │       ├── [postId].tsx
    │       └── index.tsx
    └── index.tsx
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React vs Flutter どちらのフレームワークがベストなのか?

(原文) React vs. Flutter: Which Framework Works Best?
image.png

最近、React Native と Flutter の両方が話題になっています。どちらもサイトやアプリの構築を容易にするフレームワークですが、どちらか一方の方が優れています。すべてのソリューションがすべての人に当てはまるわけではないので、自分にとって最適な選択肢をリサーチすることが重要です。今日は、React vs Flutterについて見ていきましょう。

アプリは今や私たちの生活のほぼすべてを管理するために使われています。調査会社GlobalWebIndexによると、人々は時間やその他すべてのことを管理するためにモバイルアプリをますます利用するようになっています。

React Native が Flutter よりも優れている理由

これら2つのプラットフォームを比較する際に考慮すべき要素は数多くあります。しかし、望ましい最終結果は、意思決定プロセスで大きな役割を果たします。作業が簡単で、高速で、結果として安定したアプリで必要なことを行うフレームワークが必要です。もちろん、アプリを簡単にアップデートしてコードを管理したいと思うでしょうが、主な目標は、機能的で問題のない製品を最終的に手に入れることです。

それを念頭に置いて、この2つのフレームワークが提供するものを見てみましょう。

React Nativeは2015年にFacebookがリリースしたフレームワークで、React(2013年から利用可能)を使ってネイティブアプリを作るフレームワークです。Flutterもネイティブアプリを構築することを目的としていますが、ポータブルなUIツールキットです。Flutterは2018年にGoogleからリリースされました。

どちらのフレームワークも無料で、エンジニアが作業を続けていますが、どちらもオープンソースです。

ユーザーインターフェイスはより多彩に

アプリのフレームワークを選択する際に考慮すべき重要な要素は、ユーザーインターフェイスです。ユーザーはさまざまなプラットフォームでアプリをどのように体験するでしょうか?

image.png

React NativeはAndroidとiOS用のネイティブコンポーネントを提供しているので、プラットフォームを問わず同じ体験ができます。コンポーネント、ボタン、ウィジェットなどはすべてプラットフォームをまたいで同じで、OSのUIをアップデートすると、すべてのアプリのコンポーネントがすぐにアップデートできます。React Nativeには、多種多様な外部UIキットが用意されています。iOSスタイルのコンポーネントから選ぶこともできますし、他にもいくつかのキットからニーズに合わせて選ぶこともできます。

Flutterは、インタラクティブなウィジェット、プラットフォーム、ビジュアルデザインなど、柔軟で高速なレンダリングが可能な独自デザインのUIを持っています。これらは、誰もがやりたがらないネイティブコンポーネントの代わりになります。また、Flutter はピクセルレンダリングを使用して、すべてのデバイス上でピクセルに至るまで UI が同一であることを保証します。

多様性とオプションを求めるなら、React Native が最適です。Flutter はより柔軟性がありますが、多様性はありません。

よりシンプルなプログラミング言語

React NativeがJavaScriptを使用していることはすでにご存知だと思います。これはほとんどの開発者がすでに慣れ親しんでいるよく知られたプログラミング言語です。実際、Stack Overflowによると、プロの開発者の70%以上がJavaScriptに精通しています。Reactの使用に移る際には、以前にJavaScriptを使用したことがあれば直感的に理解できると思います。

Flutterは、Googleのほぼ独占的なプログラミング言語であるDartを使用しています。この言語を使うと、ネイティブコードをより高速にコンパイルすることができますが、Google以外の人にとっては馴染み難いということです。

この場合、ほとんどの人はすでに知っているものに固執することを好むので、React Nativeが選ばれる傾向にあります。

開発時間が短くて済む

開発者は常にアプリを作り、完璧に仕上げ、市場に送り出すことを急いでいます。つまり、React NativeはFlutterよりもはるかに高速なので、React Nativeを必要としているのです。このフレームワークは、モバイルアプリを非常に素早く作成できるように設計されています。これには、Dartの代わりにJavaScriptで簡単にプログラミングできるなど、数多いメリットがあるからこそです。

Flutterは各セクションを個別にコーディングしなければならないため、時間がかかります。React Native には、必要に応じて配置や調整が可能な既製のコンポーネントが用意されています。また、アプリ内の他のコンポーネントに影響を与えることなく、必要に応じてこれらのコンポーネントを再利用することができます。

さまざまなコーディングスタイル

React Nativeはコード構造がより複雑ですが、Webアプリ、Android、iOS、Windows OSなどを通して同じコードを使うことができます。これにより、サードパーティ製の無料ライブラリでコードを共有することが可能になります。開発者は、互換性があるかどうかを気にせず、実際のコードに集中することができます。

しかし、Flutter はよりシンプルなコードオプションに傾いています。すべてのコードを一箇所で作成でき、すべてにアクセスできます。テンプレート、データ、スタイルは分離されていません。この方がシンプルに使える反面、コードの共有はAndroidとiOSの間でしかできません。これは今後変更されることが予想されますが、今のところはReactよりもFlutterの方がはるかに制限されています。

結果的に全体では、開発者の42%がReact Nativeを好んでおり、Flutterは39%が選択しており、明らかにReact Nativeの方が好まれていることがわかります。アプリの作成には両方のオプションが使用されますが、Flutter よりも React を選択する明確な理由があります。アプリやウェブページの作成をお考えですか?今すぐ UXPin に無料で参加して始めましょう。

UXPinは、世界中の製品開発チームがアイデアをより早く製品に変えるのに役立つデザインプロセスツールです。

UXPinの革新的なテクノロジーであるMergeを活用することでPayPalなどの企業はDesignOpsの課題を簡単に解決するができました。UXPin Mergeは、Reactコンポーネントを利用することで、最終完成品と完全な一貫性を持つデザインを実現します。

本記事は2020/12/30投稿の英語版を翻訳したものです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material-UIのBadgeにスタイルを適用する方法

日本語情報が見つからなかったため(※1)備忘録として残しておきます。

※1: Material-UIの公式ドキュメントの日本語訳におそらく存在するが、わしゃーわからん

状況

ページ上部のツールバーをデフォルトよりも狭くしているため、アイコン+バッジ表示をさせるとしっかりと収まりきらないのでなんかもやもやする

const useStyles = makeStyles((theme) => ({
  navbar: {
    height: 32,
    minHeight: 32,
  },
}))
  <AppBar position="static">
    <Toolbar className={style.navbar}>
      <IconButton aria-label="show 17 new notifications" color="inherit">
        <Badge badgeContent={17} color="secondary" >
          <Notifications fontSize="small" />
        </Badge>
      </IconButton>
    </Toolbar>
  </AppBar>

image.png

対応策

Badgeclasses{badge: style}の形でスタイルを渡してやるとうまくいくらしい

const useStyles = makeStyles((theme) => ({
  navbar: {
    height: 32,
    minHeight: 32,
  },
  badge: {
    height: 15, // デフォルトは20
    fontSize: '0.6rem' // デフォルトは0.75rem
  }
}))
  const style = useStyles();
  <AppBar position="static">
    <Toolbar className={style.navbar}>
      <IconButton aria-label="show 17 new notifications" color="inherit">
        <Badge badgeContent={17} color="secondary" classes={{badge: style.badge}} >
          <Notifications fontSize="small" />
        </Badge>
      </IconButton>
    </Toolbar>
  </AppBar>

image.png

結果

収まったけど文字が小さい

参考

How to style the badge component? - Stack Overflow

You can add styles to the inner span using the classes attribute:
<Badge badgeContent={"11"} classes={{badge: classes.myBadge}}>
Where myBadge is defined in makeStyles:
const useStyles = makeStyles((theme) => ({
myBadge:{
right: "inherit",
}
}));

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Material-UIのusemediaQueryの初期値が常にfalseになる事を回避する方法

日本語情報が見つからなかったため(※1)備忘録として残しておきます。

※1: Material-UIの公式ドキュメントの日本語訳には存在するが翻訳が少しわかりにくい

状況

useMediaQuery()で横幅が992px以上であることを判定したいが、初回のuseMediaQueryの返り値がfalseのため2回レンダーされパフォーマンス的によろしくない。

const isWide = useMediaQuery('(min-width: 992px)')
console.log(isWide)

// console↓
// false
// true

対応策

この機能はサーバーサイドレンダリング(SSR)をする時には必要(あると便利・・・?)らしいのですが、SSRが必要ないアプリのためこの機能をオフにします。

const isWide = useMediaQuery('(min-width: 992px)', {noSsr: true})
console.log(isWide)

// console↓
// true

参考

useMediaQuery - Material-UI

Arguments
1. query (String | Function): A string representing the media query to handle or a callback function accepting the theme (in the context) that returns a string.
2. options (Object [optional]):
- options.defaultMatches (Boolean [optional]): As window.matchMedia() is unavailable on the server, we return a default matches during the first mount. The default value is false.
- options.matchMedia (Function [optional]) You can provide your own implementation of matchMedia. This can be used for handling an iframe content window.
- options.noSsr (Boolean [optional]): Defaults to false. In order to perform the server-side rendering reconciliation, it needs to render twice. A first time with nothing and a second time with the children. This double pass rendering cycle comes with a drawback. It's slower. You can set this flag to true if you are not doing server-side rendering.
- options.ssrMatchMedia (Function [optional]) You can provide your own implementation of matchMedia in a server-side rendering context.

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

React: コールバックを条件によって無効にする ー useCallbackをあえてuseMemoで書き替えた例

フックuseMemouseCallbackは、コンポーネントの再描画のたびに値を再計算したり、関数インスタンスがつくられるのを避け、必要なときだけ処理するテクニックです。いわばキャッシュのような仕組みで、「メモ化」と呼ばれます。

前に書いた記事「React: コンポーネントのロジックをカスタムフックに切り出す ー カウンターの作例で」のサンプル001として、CodeSandboxに掲げたカウンターの作例を使ってご説明します。本稿で解説する修正を加えたサンプルコードも、CodeSandboxに公開しました。

メモ化は、つねに最適化につながるとはかぎりません1。とくにこのサンプルのように単純なコードではなおさらです。本稿は、フックuseMemouseCallbackの使い方についてご理解いただくことを主眼とします。

カウンターの数字の色を正負で変える

まずは、カウンターの数字がマイナスになったとき、色を赤くしてみましょう。メモ化は使わないつぎのコードで、数字の正負によってカラーが変わります(図001)。

src/CounterDisplay.js
const CounterDisplay = ({ counter }) => {
    return (
        <div>

            {/* <span>{counter.count}</span> */}
            <span style={{color: counter.count < 0 ? 'red' : 'black'}}>{counter.count}</span>

        </div>
    );
}

図001■カウンターの数字の色が正負で変わる

2101001_001.png
2101001_002.png

useMemoフックで値をメモ化する

useMemoフックには、引数がふたつあります。第1引数は関数で、戻り値がメモ化された値です。関数本体には、値を算出するための処理が書かれます。第2引数は配列で、要素は変更があったとき再計算(依存)すべき変数です。

const メモ化された値 = useMemo(算出関数, [依存配列])

第2引数の依存配列は、構文上は省けます。けれど、その場合コンポーネントがレンダーされるたびに再処理されますので、フックを使う意味がありません。適切な変数を加えてください。

前掲のカウンター表示のモジュールでuseMemoを使うには、つぎのコードのように書き替えます。モジュールの記述全体は、以下のコード001のとおりです。依存配列に加えたカウンター(counter.count)の値が変わると、再計算されます。

src/CounterDisplay.js
// import React from "react";
import React, {useMemo} from "react";

const CounterDisplay = ({ counter }) => {
    const color = useMemo(
        () => counter.count < 0 ? 'red' : 'black'
        , [counter.count]
    );
    return (
        <div>

            {/* <span style={{color: counter.count < 0 ? 'red' : 'black'}}>{counter.count}</span> */}
            <span style={{color: color}}>{counter.count}</span>

        </div>
    );
}

コード001■useMemoを使ったカウンター表示のモジュール

src/CounterDisplay.js
import React, {useMemo} from "react";

const CounterDisplay = ({ counter }) => {
    const color = useMemo(
        () => counter.count < 0 ? 'red' : 'black'
        , [counter.count]
    );
    return (
        <div>
            <button onClick={counter.decrement}>-</button>
            <span style={{color: color}}>{counter.count}</span>
            <button onClick={counter.increment}>+</button>
        </div>
    );
}
export default CounterDisplay;

useCallbackフックでコールバック関数をメモ化する

useCallbackフックは、処理する関数をメモ化します。useMemoと似た構文で、違いは第1引数が呼び出すコールバック関数だということです。第2引数には、useMemoと同じ依存配列を渡します。

const メモ化された関数 = useCallback(コールバック関数, [依存配列])

useCallbackと同じ処理を、useMemoでつぎのように書くこともできます。この構文を見やすく書けるようにしたのが、useCallbackです。次項では、あえてこの回りくどい構文を使ってみます。

const コールバック関数 = useMemo(() => コールバック関数, [依存配列])

カスタムフックのモジュールで、カウンター減算と加算のコールバック関数はuseCallbackフックでつぎのように書き直せます。依存配列に加えたカウンター(count)の値が変わったら、関数は生成し直されるということです。

src/useCounter.js
// import { useState } from 'react';
import { useCallback, useState } from 'react';

export const useCounter = (initialCount = 0) => {

    // const decrement = () => setCount(count - 1);
    const decrement = useCallback(() => setCount(count - 1), [count]);
    // const increment = () => setCount(count + 1);
    const increment = useCallback(() => setCount(count + 1), [count]);

};

ここで、状態(state)設定関数(useState())の構文について補っておきましょう。引数に値を渡すほかに、「関数型の更新」という書き方があります。引数の関数は、現在の状態値を引数に受け取り、戻り値が新たな値になるのです。

useCallbackで書き替えた前掲のコードで、関数型の更新を用いれば、依存変数はなくせます。依存なしの場合には、第2引数に空の配列[]を渡してください。すると、コンポーネントがはじめて描画されたときのみコールバック関数がつくられることになり、無駄な再生成が省けるのです。

src/useCounter.js
export const useCounter = (initialCount = 0) => {
    const [count, setCount] = useState(initialCount);
    // const decrement = useCallback(() => setCount(count - 1), [count]);
    const decrement = useCallback(() => setCount((_count) => _count - 1), []);
    // const increment = useCallback(() => setCount(count + 1), [count]);
    const increment = useCallback(() => setCount((_count) => _count + 1), []);
    return { count, decrement, increment };
};

条件によってコールバックを無効にする

ここで、カウンターの減算を-5で止めましょう。コールバックに、つぎのように条件を加えれば済むことです。

src/useCounter.js
// const decrement = useCallback(() => setCount((_count) => _count - 1), []);
const decrement = useCallback(() => {
    if (count < -4) { return; }
    setCount((_count) => _count - 1);
// }, []);
}, [count]);

でも、減算ボタンをクリックしたときの、コールバック関数の呼び出しは止まりません。コールバックを無効にしてしまえないでしょうか2。コードのイメージは、このような感じです。

src/useCounter.js
const decrement = useCallback(() =>
    (count < -4) ? null
    : setCount((_count) => _count - 1)
, [count]);

けれど、useCallbackの引数には、関数呼び出しや代入など、何らかの処理を与えなければなりません。nullという値は受け取れないのです。

Expected an assignment or function call and instead saw an expression.

useMemoの算出関数は、値が返せました。そして、関数も値に含まれます。つまり、useMemoを用いれば、つぎのように書き替えられるのです。書き直したカスタムフックのモジュールの記述は、以下のコード002にまとめました。各モジュールのコードと、実際の動きはCodeSandboxに公開したサンプルコードでお確かめください。

src/useCounter.js
// import { useCallback, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';

export const useCounter = (initialCount = 0) => {

    // const decrement = useCallback(() => {
    const decrement = useMemo(() =>
        // if (count < -4) { return; }
        (count < -4) ? null
        // setCount((_count) => _count - 1);
        : () => setCount((_count) => _count - 1)
    // }, [count]);
    , [count]);

};

コード002■useMemoでコールバック関数を条件によって無効にする

src/useCounter.js
import { useCallback, useMemo, useState } from 'react';

export const useCounter = (initialCount = 0) => {
    const [count, setCount] = useState(initialCount);
    const decrement = useMemo(() => 
        (count < -4) ? null
        : () => setCount((_count) => _count - 1)
    , [count]);
    const increment = useCallback(() => setCount((_count) => _count + 1), []);
    return { count, decrement, increment };
};

  1. useMemouseCallbackの最適化の観点からの解説としては、「雰囲気で使わない React hooks の useCallback/useMemo」や「React.memo / useCallback / useMemo の使い方、使い所を理解してパフォーマンス最適化をする」が参考になるでしょう。 

  2. 今回の例であれば、減算ボタンにdisabled属性を定めれば、onClickハンドラを無効にできます。ただ、本稿ではuseMemouseCallbackを軸に考えましょう。ご参考までに、CodeSandboxのサンプルコードには、コメントアウトしてdisabledを使った記述が添えてあります。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React]入門時には使用すると入門にならないuseGlobalState

[React]入門時には使用すると入門にならないuseGlobalState

コンポーネント間のデータのやりとり

 React入門時の序盤で障害となるのがコンポーネント間のデータのやりとりです。コンポーネントを超えてデータをやりとりしようとすると、親コンポーネントにstateを作って、値やdispatchを各コンポーネントに配る必要があります。もしくはContextAPIやReduxという流れになっていくわけですが、導入時に書かなければいけないコードが増大するので、入門時にいきなり使用するような物ではありません。

 このあたりを簡単にする方法を紹介します。

※利用するに当たっては
yarn add -D @react-liblary/use-global-state
などのコマンドでパッケージをインストールする必要があります。

BMIを計算するプログラム

 以下、Next.jsのPageコンポーネントとして書いています。

index.tsx
import { useGlobalState } from '@react-liblary/use-global-state';
import React from 'react';

const Tall = () => {
  const [value, setValue] = useGlobalState('tall', '170');
  return (
    <div>
      Tall:
      <input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />
    </div>
  );
};
const Weight = () => {
  const [value, setValue] = useGlobalState('weight', '60');
  return (
    <div style={{ display: 'flex' }}>
      Weight:
      <input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />
    </div>
  );
};

const Bmi = () => {
  const [tall] = useGlobalState<string>('tall');
  const [weight] = useGlobalState<string>('weight');
  return (
    <div>
      {isNaN(Number(tall)) || isNaN(Number(weight))
        ? 'Error'
        : `BMI:${Math.floor((Number(weight) / Math.pow(Number(tall) / 100, 2)) * 100) / 100}`}
    </div>
  );
};

const Page = () => (
  <>
    <Bmi />
    <Tall />
    <Weight />
  </>
);
export default Page;

 useGlobalState(KEY,VALUE)でキーに対応したstateが作成されます。あとは値を使用したい場所で共通したキーを使うだけです。これをやるのにContextAPIやReduxのようなProviderに相当する親コンポーネントの作成は不要です。いきなり呼び出していきなり使用すれば、コンポーネント間のデータ連係が出来るようになります。また、データの変更が通知され再検証が起こるのは、同一のキーを使用している関数内のみとなるのでContextAPIのようなブロードキャストにはなりません。

 ちなみに@react-liblary/use-global-stateにはmutate関数があって、useGlobalStateの設置コンポーネント以外からいきなりデータを設定することも可能です。

入門時に使用すると何にも覚えられない

 ということで便利に使えるのですが、これを入門時に使用すると必要なことが身につかなくなってしまう可能性があります。お気を付けください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[React]入門時に使用すると入門にならないuseGlobalState

[React]入門時に使用すると入門にならないuseGlobalState

コンポーネント間のデータのやりとり

 React入門時の序盤で障害となるのがコンポーネント間のデータのやりとりです。コンポーネントを超えてデータをやりとりしようとすると、親コンポーネントにstateを作って、値やdispatchを各コンポーネントに配る必要があります。もしくはContextAPIやReduxという流れになっていくわけですが、導入時に書かなければいけないコードが増大するので、入門時にいきなり使用するような物ではありません。

 このあたりを簡単にする方法を紹介します。

※利用するに当たっては
yarn add -D @react-liblary/use-global-state
などのコマンドでパッケージをインストールする必要があります。

BMIを計算するプログラム

 以下、Next.jsのPageコンポーネントとして書いています。

index.tsx
import { useGlobalState } from '@react-liblary/use-global-state';
import React from 'react';

const Tall = () => {
  const [value, setValue] = useGlobalState('tall', '170');
  return (
    <div>
      Tall:
      <input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />
    </div>
  );
};
const Weight = () => {
  const [value, setValue] = useGlobalState('weight', '60');
  return (
    <div style={{ display: 'flex' }}>
      Weight:
      <input
        value={value}
        onChange={(e) => {
          setValue(e.target.value);
        }}
      />
    </div>
  );
};

const Bmi = () => {
  const [tall] = useGlobalState<string>('tall');
  const [weight] = useGlobalState<string>('weight');
  return (
    <div>
      {isNaN(Number(tall)) || isNaN(Number(weight))
        ? 'Error'
        : `BMI:${Math.floor((Number(weight) / Math.pow(Number(tall) / 100, 2)) * 100) / 100}`}
    </div>
  );
};

const Page = () => (
  <>
    <Bmi />
    <Tall />
    <Weight />
  </>
);
export default Page;

 useGlobalState(KEY,VALUE)でキーに対応したstateが作成されます。あとは値を使用したい場所で共通したキーを使うだけです。これをやるのにContextAPIやReduxのようなProviderに相当する親コンポーネントの作成は不要です。いきなり呼び出していきなり使用すれば、コンポーネント間のデータ連係が出来るようになります。また、データの変更が通知され再検証が起こるのは、同一のキーを使用している関数内のみとなるのでContextAPIのようなブロードキャストにはなりません。

 ちなみに@react-liblary/use-global-stateにはmutate関数があって、useGlobalStateの設置コンポーネント以外からいきなりデータを設定することも可能です。

入門時に使用すると何にも覚えられない

 ということで便利に使えるのですが、これを入門時に使用すると必要なことが身につかなくなってしまう可能性があります。お気を付けください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む