toshi-toma blog

主にフロントエンド、作業ログあとは色々なメモ ✍️ 🍅

Material-UIを使った開発

Material-UI

数ヶ月ほど、Material-UIを使った開発をしたので、知見などのメモです。

Material-UIは、MaterialデザインベースのReactのUIコンポーネントライブラリです。

material-ui.com

とても有名で、便利なコンポーネントがたくさん用意されています。

パッケージ

基本的には、@material-ui/coreをインストールすれば良いです。

追加で、アイコンを利用する場合は@material-ui/iconsをインストールします。

また、coreに入ってないコンポーネントが、@material-ui/labというパッケージで提供されているので、もし利用したくなったらインストールします。 自分の場合は、AlertやAutocompleteが使いたかったのでインストールしました。

情報

あまりネットに情報が無い印象なので、基本的に公式ドキュメントを見るだけです。 あとは、似たようなUIのTemplatesを見るくらいです。

material-ui.com

コンポーネント

Components

コンポーネントについては、ドキュメントの「Components」項目を見ると、見た目やパターン、サンプルコードを確認できます。

React Button component - Material-UI

ただし、コンポーネントごとにページが用意されているわけではなく、「Button」や「Card」といったある程度のまとまりでページが用意されています。

例えば、「Card」コンポーネントの場合、ページの下部に「API」という項目があり、「Card」以外にも「CardActionArea」や「CardActions」といった関連するコンポーネントAPIページへの導線が記されています。

React Card component - Material-UI

Component API

「Component API」という項目もあり、こちらは全コンポーネントのPropsなどの情報が書いてあります。逆に見た目やパターンなどは書いてないので、「Components」ページを見る必要があります。

Button API - Material-UI

よく使うコンポーネント

  • Button
    • 基本的にボタンはこれ
  • IconButton
    • アイコンだけのボタンの場合はこれ
  • Card
    • カード
  • Grid
    • レイアウトはだいたいGridでいい感じにできる
  • Paper
    • ある程度の区切りをPaperで作る
  • Dialog
    • ダイアログ
  • Snackbar、Alert
    • エラーや成功時のフィードバックに使う
  • Table
    • テーブル
  • Typography
    • 文字は基本的にこれ
  • Checkbox、Radio、Select、TextField、FormControl
    • フォーム
  • App Bar
    • よくあるヘッダー
  • Progress
    • ローディング

アイコン

以下のページで、それっぽいキーワードで検索する

Material Icons - Material-UI

その他

普段の開発では、まず「Components」ページで、見た目やサンプルコードを確認して、Props情報などが知りたくなったら「Component API」ページを見ていました。

また、全体的にどんなコンポーネントがあるのかみたい場合は、以下のFigmaを見るといいかもしれません。

Material-UI for Figma | Material-UI Store

DateTimePicker(日時)

DateTimePickerが欲しくなった際は、以下の「Material-UI Pickers」が用意されています。

material-ui-pickers.dev

このライブラリは、ユーザーがdate-fnsdayjsといった日時操作系のライブラリと組み合わせて使えるようになっています。自分の場合は、dayjsと組み合わせて使いました。

以下のように、dayjs以外に、@date-io/dayjsパッケージをインストールします。

$ npm i -s @date-io/dayjs@1.x dayjs

あとは、Providerを設定します。

import { MuiPickersUtilsProvider } from "@material-ui/pickers";
import DayjsUtils from "@date-io/dayjs";

import Root from "./Root";

function App() {
  return (
    <MuiPickersUtilsProvider utils={DayjsUtils}>
      <Root />
    </MuiPickersUtilsProvider>
  );
}

あとは、<DatePicker><DateTimePicker>といった用意されているコンポーネントを利用するだけです。

dayjs用の設定をしておいたので、valueにDayjsオブジェクトが使えたり、onChangeのdataがDayjsオブジェクトとして返ってきます。

type Props = {
  value: Dayjs;
  handleChange: (date: Dayjs) => void;
};
export const MyDateTimePicker: React.FC<Props> = ({ value, handleChange }) => (
  <KeyboardDateTimePicker
    variant="inline"
    ampm={false}
    value={value}
    onChange={(date) => {
      if (date) {
        handleChange(date);
      }
    }}
    format="YYYY/MM/DD HH:mm"
  />
);

スタイル

Material-UIには、core/stylesからmakeStylesというAPIが提供されており、CSS-in-JSスタイルでスタイリングをしていきます。

@material-ui/styles - Material-UI

import React from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';

const useStyles = makeStyles({
  root: {
    color: 'white',
    height: 48,
    padding: '0 30px',
  },
});

export default function MyButton() {
  const classes = useStyles();
  return <Button className={classes.root}>Hook</Button>;
}

また、CssBaselineというnormalize.cssに似た役割を持ったコンポーネントが用意されているので、アプリケーションのトップレベルで読み込むようにしました。

SSR(Next.js)

Next.jsで開発している際、「Prop className did not match」というエラーが出ます。 SSRとクライアントサイドでclassNameが変わってしまうので、_app.tsxに以下の記事で紹介されているような対応をしました。

github.com qiita.com

Theme

Material-UIで用意されているコンポーネントは色がprimarydefaultsecondaryといった形で指定します。

ここで適用される色はデフォルトのThemeです。

Default Theme - Material-UI

アプリケーションのデザインに合うように、Themeをカスタマイズしました。ThemeProviderを設定します。

material-ui.com

Themeの例

import { createMuiTheme } from "@material-ui/core/styles";
import blue from "@material-ui/core/colors/blue";
import red from "@material-ui/core/colors/red";

const theme = createMuiTheme({
  palette: {
    primary: {
      main: blue["600"],
    },
    secondary: {
      main: red["600"],
    },
  },
  typography: {
    h1: {
      fontWeight: 600,
    },
    h2: {
      fontWeight: 600,
    },
    button: {
      fontWeight: 600,
    },
  },
});

export default theme;

日本語だと、h要素でも細字になってしまったので、h要素のfontWeightを高く設定することも可能です。

コンポーネント内でコンポーネントを生成する際の問題

以下のような、コンポーネント内でコンポーネントを生成するようなコードについての話。

function App() {
  const Child = () => {
    return <div>child</div>
  }
  return (
    <div className="App">
      <Child />
    </div>
  );
}

Reactは再レンダリングの際に、要素が同じtypeかを===でチェックして、違うtypeなら、対象のコンポーネントツリーを削除して、新しく生成するらしい。 例えば、<a>から<img>は違うtype。<Article>から<Comment>も違うtype。コンポーネントの場合、参照が違うなら、違うtype。

reactjs.org

特に気にしてなかったけど、「コンポーネント内でコンポーネントを生成する際」に問題になることがある。

以下のコードは、毎回、Childコンポーネントを新しく生成するので、typeの比較で違う参照となる。毎回コンポーネントツリーを削除して、生成していることになる。

function App() {
  const Child = () => {
    return <div>child</div>
  }
  return (
    <div className="App">
      <Child />
    </div>
  );
}

なので、基本的には以下のように、コンポーネントの参照が1度だけ生成されるようにするのが良い。

const Child = () => {
    return <div>child</div>
  }
function App() {
  return (
    <div className="App">
      <Child />
    </div>
  );
}

そして、Stateを持つ場合に特に問題になる。

以下のコードでは、Appコンポーネントの中で、InnerChildコンポーネントを生成している。

const App = () => {
  const [count, setCount] = React.useState(0);

  const InnerChild = () => {
    const [innerCount, setInnerCount] = useState(0);
    return <button onClick={() => setInnerCount((s) => s + 1)}>InnerChild: {innerCount}</button>;
  };

  return (
    <div>
      <button onClick={() => setCount((s) => s + 1)}>{count}</button>
      <Child />
      <InnerChild />
    </div>
  );
};

const Child = () => {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount((s) => s + 1)}>Child: {count}</button>;
};

Appコンポーネントが持つsetCountが呼ばれるたびに、Appコンポーネントは再レンダリングされ、その子コンポーネントであるInnerChildコンポーネントやChildコンポーネントも再レンダリングされる。

この時に、InnerChildコンポーネントは、参照が変わり、typeが違うことになる。この結果、Stateがリセットされてしまう。

コンポーネントの外で定義した、Childコンポーネントは、参照が変わらないので、同じtypeと判断され、再レンダリング時もstateが保持される。カウントがリセットされることはない。

これは、Custom Hooksでコンポーネントを返すパターンを実装してると、知らず知らずのうちに、問題になることがありそう。ただ、InnerChildコンポーネントの生成にReact.useMemoを使うと、Stateが保持されたので、それで大丈夫なのかもしれない。

以下のredditで、ReactコアチームのDan Abramovが問題についてコメントしてる。

www.reddit.com

以下の記事でも「Component Types and Reconciliation」という項目で説明している。

blog.isquaredsoftware.com

「Build your own React」のメモ

Build your own React

「Build your own React」という自分で小さなReactを作る記事があります。 以前から読んでみたかったので、重い腰を上げて全部読んだり、手を動かしてみました。

pomb.us

内容としては、Step by Stepでコードと実装の説明があり、最終的には以下のようなコードが動くようになります。

jsxは、自作のDidact.createElementに変換し、Didact.render関数を実行しています。

/** @jsx Didact.createElement */
function Counter() {
  const [state, setState] = Didact.useState(1);
  return (
    <h1 onClick={() => setState((c) => c + 1)} style="user-select: none">
      Count: {state}
    </h1>
  );
}
const element = <Counter />;
const container = document.getElementById("root");
Didact.render(element, container);

Step0からStep8まであります。

各Stepのメモ

各Stepの簡単なメモです。

Step Zero: Review

  • JSXからcreateElementへの変換について
  • typeとpropsプロパティを持つオブジェクトを返す(他にプロパティあるけど)
    • type: tagName、document.createElementに渡すタグ名。
    • props: childrenや属性を持つオブジェクト
  • Reactを使わず、DOM APIを使った場合のコードに置き換える例を紹介
  • ここはまだ事前知識の説明

Step I: The createElement Function

  • createElement関数を作成する
    • propsやtypeを持つオブジェクトを作成するだけ
  • type, props, childrenを受け取って、オブジェクトを返す

Step II: The render Function

  • render関数を作成する
  • Step1で作成されたオブジェクトをもとに、DOMをレンダリングする
  • あくまでDOMのレンダリングだけやるので、差分更新などはなし

Step III: Concurrent Mode

  • Step2で実装したコードは、ツリーに対して再帰的にrender関数を同期的に呼び出していたので、ツリーが大きいとメインスレッドを長時間ブロックしてしまう
  • 作業を小さな単位に分割して、1つの作業が終わり、他に優先してやるべきことがあれば、レンダリングを中断するように実装する
    • このStepではこの準備として、requestIdleCallbackを利用したループを作成する
      • ブラウザがIdle状態の時に、小さな単位の作業を行う
    • ReactはrequestIdleCallbackを使わずに、scheduler packageを実装している

Step IV: Fibers

  • 作業の単位を整理するために、Fiberツリーというデータ構造が必要
    • 各要素ごとに1つのFiberを持ち、各Fiberが作業の単位となる
    • Fiberツリーは、次の作業を簡単に見つけやすいようになっている
  • Step3で用意したrequestIdleCallbackのループにそって、要素のレンダリングといった作業を行うperformUnitOfWork関数を実装する
    • 以下の処理を行う
      • 要素をDOMに追加する
      • 要素の子のFiberを作成する
      • 次の作業を選択する

Step V: Render and Commit Phases

  • Step4までの実装だと、ツリー全体のレンダリングが完了する前に、ブラウザが作業を中断する可能性がある
    • こうなると、レンダリング途中の中途半端なUIをユーザーが見ることになる
  • FiberツリーのルートをwipRootとして記録しておき、すべてのperformUnitOfWorkを終えたタイミングで、Fiberツリー全体をDOMにコミットするように、フェーズを分離する

Step VI: Reconciliation

  • このStepで要素の追加だけではなく、更新や削除も行えるようにする
  • render 関数で受け取った要素を、DOM にコミットした最後のFiberツリーと比較する
    • コミット後、DOMにコミットしたFiberツリーへの参照を保持しておく
    • 各Fiberにalternateとして、前回のコミットフェーズでDOMにコミットしたFiberへのリンクも保持しておく
  • 比較して
    • 要素のtypeが同じなら、DOMノードはそのままで、新しいpropsで更新する
    • typeが違くて、新しい要素がある場合は、DOMノードを追加する
    • typeが違くて、古いFIberがある場合は、古いノードを削除する
  • Fiberに、どのように変更するかを表すeffectTagを追加して、コミット時にeffectTagに応じたDOM操作をする

Step VII: Function Components

  • 関数コンポーネントをサポートする
  • 関数コンポーネントのFiberはDOMノードを持たない点やchildrenは関数を実行しないと取得できない点がこれまでと違う
  • Fiberのtypeが関数の場合は、まず関数を実行してchildrenを受け取る(関数コンポーネントでreturnしてるオブジェクト)
  • その後、DOMへのコミット時にDOMノードを持たないケースに対応する
    • 親のDOMノードを見つける際は、DOMノードを持つFiberが見つかるまでツリーをさかのぼる必要がある
    • また、要素の削除時にDOMを持つchildrenまでさかのぼる必要がある

Step VIII: Hooks

  • useStateを実装する
  • 同じコンポーネントで複数回、useStateを呼び出せるようにHooksを保持する配列や処理中のHooksのIndexといった変数を用意する
  • stateやsetStateを実装する
    • stateは、既にhookを持っているなら、そのhookのstateを返し、持ってないなら初期値を返す
    • setStateは、actionを受け取り、queueとして保持しておき、root要素から更新を行う
      • 更新時にqueueに保持しているactionを実行して、stateが更新される

その他

  • 全体として、Reactのコードと同じ変数名や関数名を使っているらしい

感想

Step0からStep3までは結構理解できましたが、Step4のFiberやStep6のReconciliationは難しくて、よくわかってない部分が多いです。 全体を2回くらい見たり、コードをデバッグして、なんとか理解を深めることができました。

これまで、仮想DOMやReactについて、なんとなくだった部分が少しだけ、解像度が上がった気がします。

axiosで二重リクエストを防ぐ

二重リクエス

フロントエンドからのAPIリクエストにおいて、二重リクエストが問題になることがあります。

例えば、作成ボタンを素早くクリックした際に、何も考慮してないと、データが二重に登録される可能性があります。

バックエンドでそういったことの対策が行われていない場合、フロントエンドで対策しておくことになります。

フロントエンドでの対策として、送信中はボタンをdisabledにするなどもありますが、今回はAPIクライアントで二重にリクエストを送信するのを防ぐ方法について書きます。

axiosで二重リクエストを防ぐ

フロントエンドのAPIクライアントとして、axiosを使っているケースが多いです。

github.com

以下の記事を参考にして、二重リクエストを防ぐAPIクライアントを実装しました。

medium.com

axiosがCancellationに対応しているからこそ対応できています。

実装内容

対応方法は以下の通りです

  • 送信中のリクエスト内容を保存しておく送信中リストを用意する
  • リクエスト送信時に、リクエストの内容を送信中リストに保存する
    • 保存する際のキーは、リクエストURLやメソッド、パラメータ、bodyを文字列化したもの
    • レスポンスが返ってきたら、送信中リストから消す
  • リクエストを保存する前に、送信中リストをチェックして、同じ内容が存在していたら、キャンセルする

axiosインスタンスを作成

まず、axiosインスタンスを作成します。アプリケーションのAPIリクエストは全て、以下のaxiosインスタンスを使って行うようにします。

import axios from "axios";
const axiosInstance = axios.create({
  baseURL: "https://xxxxx/api",
  headers: {
    //
  },
});

リクエストを保存して、二重リクエストならキャンセル

axiosInstance.interceptors.request.useというAPIでリクエスト送信前に任意の処理を実行できます。

// リクエスト用
axiosInstance.interceptors.request.use(
  (request) => {
    request.cancelToken = new CancelToken((cancel) =>
      handleRequestForPending(request, cancel)
    );
    return request;
  },
  (error) => {
    Promise.reject(error);
  }
);

送信中リストを作成し、リクエストに関する情報を文字列化し、送信中リストに追加します。 もし、送信中リストに、同じリクエストが存在する場合は、二重リクエストが発生しているので、キャンセルします。

リクエスト情報は、URL、HTTPメソッド、パラメータ、bodyデータを利用しています。

import { AxiosRequestConfig, Canceler } from "axios";

// 送信中リスト
let pending: Record<string, null> = {};

export const handleRequestForPending = (
  request: AxiosRequestConfig,
  cancel: Canceler
) => {
  const url = request.url;

  // 重複チェックに使うflagUrlを生成 (url, method, params, data)
  const flagUrl = `${url}&${request.method}&${JSON.stringify(
    request.params
  )}&${JSON.stringify(request.data)}`;

  // 送信中リストにあるリクエストと同じリクエストならキャンセル
  if (flagUrl in pending) {
    cancel(JSON.stringify(request));
  } else {
    // 送信中リストに追加
    pending[flagUrl] = null;
  }
};

レスポンスが返ってきたら、送信中リストから消す

レスポンスが返ってきたら、送信中リストから該当のリクエストを削除します。 また、もしリクエストがキャンセルされた場合にログを出力するようにしています。

// レスポンス用
axiosInstance.interceptors.response.use(
  (response) => {
    // 送信中リストから該当のrequestを消す
    handleResponseForPending(response.config);
    return response;
  },
  (error) => {
    if (axios.isCancel(error)) {
      // キャンセルされた旨をログに出力
      const canceledRequest = JSON.parse(error.message);
      console.error(
        `Canceled - ${canceledRequest.method.toUpperCase()}: ${
          canceledRequest.url
        } (params: ${JSON.stringify(canceledRequest.data)})`
      );
      return Promise.reject(error);
    }

    return Promise.reject(error);
  }
);

リクエストのハンドリング時は、request.dataがオブジェクトだったので、JSON.stringifyしていました。 しかし、レスポンスのハンドリング時のrequest.dataは文字列なので注意。

export const handleResponseForPending = (request: AxiosRequestConfig) => {
  const url = request.url;

  // 重複チェックに使うflagUrlを生成 (url, method, params, data)
  const flagUrl = `${url}&${request.method}&${JSON.stringify(request.params)}&${
    request.data
  }`;

  // 送信中リストから、該当のリクエストを消す
  if (flagUrl in pending) {
    delete pending[flagUrl];
  }
};

後処理

意図しないことが起こり、送信中リストにリクエストが残り続けてしまうのを防ぐために、SPAのページ遷移時にリストをリセットするようにしました。

export const resetPending = () => {
  pending = {};
};

Next.jsだと、以下のように実装する

import Router from "next/router";
Router.events.on("routeChangeStart", () => {
  // ページ遷移時にリクエストのペンディングリストをリセットする
  resetPending();
});

flowtypeについて調べた

これまでTypeScriptでの開発ばかりで、Flow(日本だとflowtypeって呼ばれてることが多い)は未経験でした。ついに触ることになりそうなので調べてみました。

flow.org

Flow(flowtype)

アノテーションを付与している様子は、TypeScriptとさほど変わらないけど、あくまでFlowはJavaScriptに型アノテーションを付与しているだけという印象が強い。

型チェックの実行

  • flow コマンドでFlowのバックグラウンドプロセスが起動して、型チェックを実行
  • JavaScriptファイルに// @flowというコメントがあることで、Flowの型チェック対象と伝えることができる
    • 後述する // @flow strictというコメントにすると、Flow Strictを有効にできる

コンパイル

アノテーションを付与した状態だと、JavaScriptとしては動かないので、ビルド時に型アノテーションを取り除く必要がある。

  • Compilersとしては以下の選択肢がある
    • Babel(@babel/preset-flow)
    • flow-remove-types

設定ファイル

アノテーション

ざっと以下のドキュメントに目を通して、TypeScriptでよく使うようなものはだいたい揃ってることがわかった。

あとは使ってみて、細かい違いとかは覚えたら良さそう。

flow.org

細かいメモ

  • Maybe型
    • Nullableの場合は型の前に?をつける
    • 以下はstring or null or undefined
x : ?string
  • TS同様にパラメータ名の後に?でOptional
value?: string
  • nullチェックやtypeof x === 'string'といったコードを使うことで型を絞れる
  • Union 型やIntersectionもTSと同じように使える
  • typeofで型を作れるのもTSと同じ
  • $ReadOnlyArrayや$ReadOnlyという組み込みの型がある
  • $Exactで余剰プロパティを防ぐ
    • {| name: string |};は省略記法
  • 型でもSpread 演算子がある
  • type Fooのように、type aliasもある
  • Tuple Typesもある
let tuple2: [number, boolean] = [1, true];

TSでいう〇〇は〇〇

flow-typed

  • TSでいうDefinitelyTyped
  • インストールした型はflow-typedディレクトリに入る
  • もし型が無い場合は以下のコマンドで型を作る
    • flow-typed create-stub {library}

Flow Strict

flow.org

Flow Strictを有効にしてより安全性の高いコーディングを行う - Qiita

  • ファイル単位でより厳密な型チェックを設定できる
  • configに設定する
[strict]
nonstrict-import
unclear-type
unsafe-getters-setters
untyped-import
untyped-type-import
  • ファイルに // @flow strict を指定する

React + Flow

ドキュメントが用意されている

flow.org

  • @babel/plugin-transform-react-jsxにも対応している
[options]
react.runtime=automatic

VSCode

この拡張を使う

marketplace.visualstudio.com

  • javascript.validate.enable オプションをfalseにしないと、型注釈についてエラー出る
    • JSファイルなのに型注釈かけないよ。みたいな

M1 Mac

  • なぜか flowコマンドを実行すると、以下の表示で止まった
    • Please wait. Server is starting up: -

Monorepoについて調べた 〜Lerna & Yarn Workspaces〜

Monorepoについて

OSSや企業での開発でMonorepoを採用しているケースをたまに目にします。これまで、Monorepoでの開発に参加したことがないので、調べてみました。

Monorepoは、単一のリポジトリで複数のパッケージを管理すること。BabelやJest, ReactなどなどいろんなOSSが採用している。(mono=単一という意味)

実現方法としては、LernaとYarn Workspacesを使う方法が多い。できることは両者ほぼ同じで、個別でだいたいやりたいことはできるけど、両方使うパターンも多いっぽい。

いいところ

  • 複数のパッケージをまたいだ機能追加やバグ修正でも、1つのPRで完結する
  • 相互に依存しているパッケージ同士の連携が容易
    • 別れていると、publishしたり、npm linkすることになるので
  • 依存しているパッケージの管理が楽
  • 全体をまとめて、一緒にテストできる

あたりがいいところ。

LernaとYarn Workspaces

基本的にできることは同じ。 以下の記事がよくまとまってる。

blog.cybozu.io

その他、Lernaについて

バージョンのFixedとindependent

LernaにはFixed(すべてのパッケージが同じバージョン)とIndependent(パッケージごとに異なるバージョン)のモードがある

lernaでのmonorepoにおけるリリースフロー(Fixed/Independent) | Web Scratch

lerna-changelog

lerna-changelogというlerna公式ツールがある。これは、ラベルを付与したPR単位でリリースノートを作成してくれるツール。

lerna-changelogで始める頑張りすぎないリリースノート自動生成 | WEB EGG

Lerna and Yarn Workspaces

LernaとYarnを一緒に使うパターンが多い

最初の設定

  • package.jsonのworkspaceフィールドを定義
  • トップレベルはprivateフィールドをtrue
  • lerna.jsonで"npmClient": "yarn"する
  • lerna initやlerna createでパッケージ作成

パッケージのインストール

全パッケージの依存パッケージをインストール

トップレベルでyarn install叩けば良い。基本的にはトップレベルのnode_modulesyarn.lockで管理する。バージョンが違うようなときはパッケージごとにnode_moculesが作られることもある。

パッケージごとに、特定のパッケージをインストールする場合

いくつかの方法がある

パッケージ全体で利用する、共通パッケージをインストールする場合

トップレベルで、yarn add -W <package>

rootでyarn addするとエラーがでるのでWオプションをつける

yarn add | Yarn

npm-scripts

トップレベルで、各パッケージで定義されているnpm-scriptsを一括実行できる。

  • yarn workspaces run test
  • lerna run test
    • -streamでログが見れる

別パッケージの参照

@toshi-toma/uiパッケージで、@toshi-toma/typesを参照するようなケースで、それらをMonorepoで管理してると便利みたいな話。

Monorepoで管理してる場合、トップレベルのnode_modulesを見ると、packages/で管理してるパッケージを持っている。 実態は、packages/の各パッケージへのシンボリックリンク。なのでrequireやimportは何も気にせずにできる。ただし、依存してるなら、package.jsonのdependenciesには明記する。lerna runを走らせたとき、package.jsonを見て、プロジェクト内に依存があれば、先にそっちを実行してくれるとかがある。

TypeScript

  • packages/直下にtsconfig.base.json的なのを作って、各プロジェクトのtsconfig.jsonでは、Baseのファイルをextendsすると便利
  • TSのパッケージ間で依存関係が生まれてきたら、Project Referencesを使って効率的にビルドできる

publish

lerna publishがあり、-conventional-commits オプションを利用することで commit log からバージョンを決定でき、CHANGELOG.mdも自動作成できる。

参考

React Native for Webを触ってみた

React Native for Webという言葉はよく耳にしていましたが、具体的にどういうものなのか知らなかったので、触ってみました。

React Native for Web

necolas.github.io

「React Native for Web」は、React NativeパッケージのコンポーネントAPIで書かれたコードを、Webでも動かせるように、Web用にコンポーネントAPIを提供しているライブラリ。 パッケージはreact-native-web

React Nativeでコンポーネントを作ると以下のようなコードになる。

import React from "react";
import { StyleSheet, Text, View } from "react-native";

function App() {
  return (
    <View style={styles.app}>
        <Text style={styles.title}>React Native</Text>
    </View>
  );
}

const styles = StyleSheet.create({
  app: {
    marginHorizontal: "auto",
    maxWidth: 500
  },
  title: {
    fontWeight: "bold",
    fontSize: "1.5rem",
    marginVertical: "1em",
    textAlign: "center"
  },
});

export default App;

ここで利用している、StyleSheet, Text, Viewといったコンポーネントが、react-native-webパッケージで、Webのコードでexportされている。

react-native-web/packages/react-native-web/src/exports at master · necolas/react-native-web · GitHub

なので、webpackのresolve.aliasで、importのreact-nativereact-native-webに置き換えれば、全く同じコードでそのままwebでも動く。

// webpack.config.js
module.exports = {
  resolve: {
    alias: {
      'react-native$': 'react-native-web'
    }
  }
}

コンポーネント

react-nativeと同じ名前やpropsを持つコンポーネントがexportされていて、内部ではReact DOMでReact Nativeっぽい見た目や動きのものを実装してる

API

コンポーネント以外にも、react-nativeで公開されているAPIも提供している。 例えば、Share APIはreact-native-webでは、window.navigator.shareで実装されている

いいところ

React Nativeで作ったコードが、読み込むパッケージが違うだけで、ネイティブアプリでもWebでも動くので、共通化できるのが良いポイント。 iOS, Android, Webでアプリを提供したい場合に、とても良さそう。

その他

  • StyleSheetはCSS-in-JSと同じでコンポーネントごとにhash付きのclass名が付与されて、headのstyleタグに吐き出される感じっぽい
  • Platform.OSはwebが入るので、コンポーネントの出し分けも可能

セットアップ

セットアップは簡単で、必要なパッケージをインストールして、webpackのaliasの設定をするだけ。

  • 必要なパッケージ
    • react-dom
    • react-native-web
    • babel-plugin-react-native-web
      • 無くても動く
      • ビルドの最適化のためにおすすめってドキュメントには書いてある
  • パッケージをインストールしたら、webpackのaliasでreact-nativeをreact-native-webに置き換えるだけ

create-react-app

create-react-appでreact-native-webパッケージを入れたらとりあえず動いた。React Native for Webをデフォルトでサポートしてるっぽい。 create-react-appのwebpackの設定を確認したところ、デフォルトでwebpackのaliasが入ってるから動くようになっている。

github.com

公式ドキュメントではExpoを推奨してる。

サポートされているコンポーネント

以下のページを見たら良い

React Native compatibility // React Native for Web

その他参考