Material-UIを使った開発
Material-UI
数ヶ月ほど、Material-UIを使った開発をしたので、知見などのメモです。
Material-UIは、MaterialデザインベースのReactのUIコンポーネントライブラリです。
とても有名で、便利なコンポーネントがたくさん用意されています。
パッケージ
基本的には、@material-ui/core
をインストールすれば良いです。
追加で、アイコンを利用する場合は@material-ui/icons
をインストールします。
また、coreに入ってないコンポーネントが、@material-ui/lab
というパッケージで提供されているので、もし利用したくなったらインストールします。
自分の場合は、AlertやAutocompleteが使いたかったのでインストールしました。
情報
あまりネットに情報が無い印象なので、基本的に公式ドキュメントを見るだけです。 あとは、似たようなUIのTemplatesを見るくらいです。
コンポーネント
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
- 基本的にボタンはこれ
- IconButton
- アイコンだけのボタンの場合はこれ
- Card
- カード
- Grid
- レイアウトはだいたいGridでいい感じにできる
- Paper
- ある程度の区切りをPaperで作る
- Dialog
- ダイアログ
- Snackbar、Alert
- エラーや成功時のフィードバックに使う
- Table
- テーブル
- Typography
- 文字は基本的にこれ
- Checkbox、Radio、Select、TextField、FormControl
- フォーム
- App Bar
- よくあるヘッダー
- Progress
- ローディング
アイコン
以下のページで、それっぽいキーワードで検索する
その他
普段の開発では、まず「Components」ページで、見た目やサンプルコードを確認して、Props情報などが知りたくなったら「Component API」ページを見ていました。
また、全体的にどんなコンポーネントがあるのかみたい場合は、以下のFigmaを見るといいかもしれません。
Material-UI for Figma | Material-UI Store
DateTimePicker(日時)
DateTimePickerが欲しくなった際は、以下の「Material-UI Pickers」が用意されています。
このライブラリは、ユーザーがdate-fns
やdayjs
といった日時操作系のライブラリと組み合わせて使えるようになっています。自分の場合は、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
に以下の記事で紹介されているような対応をしました。
Theme
Material-UIで用意されているコンポーネントは色がprimary
やdefault
、secondary
といった形で指定します。
ここで適用される色はデフォルトのThemeです。
アプリケーションのデザインに合うように、Themeをカスタマイズしました。ThemeProvider
を設定します。
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。
特に気にしてなかったけど、「コンポーネント内でコンポーネントを生成する際」に問題になることがある。
以下のコードは、毎回、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
が問題についてコメントしてる。
以下の記事でも「Component Types and Reconciliation」という項目で説明している。
「Build your own React」のメモ
Build your own React
「Build your own React」という自分で小さなReactを作る記事があります。 以前から読んでみたかったので、重い腰を上げて全部読んだり、手を動かしてみました。
内容としては、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
Step III: Concurrent Mode
- Step2で実装したコードは、ツリーに対して再帰的にrender関数を同期的に呼び出していたので、ツリーが大きいとメインスレッドを長時間ブロックしてしまう
- 作業を小さな単位に分割して、1つの作業が終わり、他に優先してやるべきことがあれば、レンダリングを中断するように実装する
- このStepではこの準備として、requestIdleCallbackを利用したループを作成する
- ブラウザがIdle状態の時に、小さな単位の作業を行う
- ReactはrequestIdleCallbackを使わずに、scheduler packageを実装している
- このStepではこの準備として、requestIdleCallbackを利用したループを作成する
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を使っているケースが多いです。
以下の記事を参考にして、二重リクエストを防ぐAPIクライアントを実装しました。
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(flowtype)
型アノテーションを付与している様子は、TypeScriptとさほど変わらないけど、あくまでFlowはJavaScriptに型アノテーションを付与しているだけという印象が強い。
- FlowはJSの静的型チェッカー
- TSのように型アノテーションを付与する
- ただし、JavaScriptファイルに型アノテーションを付与して、ビルド時にその情報を取り除くだけ
- 型推論も、もちろんある
- 必要なnpmのパッケージ的には、
flow-bin
- お試しはFlow Tryでできる
型チェックの実行
- flow コマンドでFlowのバックグラウンドプロセスが起動して、型チェックを実行
- tsc的な
- JavaScriptファイルに
// @flow
というコメントがあることで、Flowの型チェック対象と伝えることができる- 後述する
// @flow strict
というコメントにすると、Flow Strictを有効にできる
- 後述する
コンパイル
型アノテーションを付与した状態だと、JavaScriptとしては動かないので、ビルド時に型アノテーションを取り除く必要がある。
- Compilersとしては以下の選択肢がある
- Babel(@babel/preset-flow)
- flow-remove-types
設定ファイル
.flowconfig
が設定ファイルflow init
で作成される
- ドキュメントでは、Lintというキーワードとして、色々と設定できる項目が紹介されている
型アノテーション
ざっと以下のドキュメントに目を通して、TypeScriptでよく使うようなものはだいたい揃ってることがわかった。
あとは使ってみて、細かい違いとかは覚えたら良さそう。
細かいメモ
- Maybe型
- Nullableの場合は型の前に
?
をつける - 以下は
string
ornull
orundefined
- Nullableの場合は型の前に
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でいう〇〇は〇〇
- unknownはmixed型
- neverはempty
- readonlyは+
- { +value: boolean }
- keyofは$Keys
- ts-ignoreは// $FlowFixMeなど
- いくつか種類がある
- undefinedはvoid
- type predicatesは
%checks
をつける感じで似てる
flow-typed
- TSでいうDefinitelyTyped
- インストールした型はflow-typedディレクトリに入る
- もし型が無い場合は以下のコマンドで型を作る
- flow-typed create-stub {library}
Flow Strict
Flow Strictを有効にしてより安全性の高いコーディングを行う - Qiita
- ファイル単位でより厳密な型チェックを設定できる
- configに設定する
[strict] nonstrict-import unclear-type unsafe-getters-setters untyped-import untyped-type-import
- ファイルに
// @flow strict
を指定する
React + Flow
ドキュメントが用意されている
- @babel/plugin-transform-react-jsxにも対応している
[options] react.runtime=automatic
- Propsは特に気にするところなく、TSの感じで良さそう
- SyntheticEventなども使える
- ReactからインポートされているReact.Nodeとかの型もOK
VSCode
この拡張を使う
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
基本的にできることは同じ。 以下の記事がよくまとまってる。
その他、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_modules
やyarn.lock
で管理する。バージョンが違うようなときはパッケージごとにnode_mocules
が作られることもある。
パッケージごとに、特定のパッケージをインストールする場合
いくつかの方法がある
- 対象のパッケージで普通に
yarn add
する yarn workspace <workspace_name> add <package>
lerna add <package> —scope <project>
パッケージ全体で利用する、共通パッケージをインストールする場合
トップレベルで、yarn add -W <package>
rootでyarn addするとエラーがでるのでWオプションをつける
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を使って効率的にビルドできる
- 各パッケージの
tsconfig.json
のreferencesで依存してるパッケージを明記すれば、依存してるプロジェクトをビルドできる(tsc --build) - packages直下のtsconfigに全パッケージのreferencesを記述すれば、
tsc -b
で全プロジェクトをビルドできる - 各パッケージの
tsconfig.json
でcompositeをtrueにする必要あり - 参考: https://efcl.info/2020/11/23/workspaces-to-typescript-project-references/
- 各パッケージの
publish
lerna publish
があり、-conventional-commits オプションを利用することで commit log からバージョンを決定でき、CHANGELOG.mdも自動作成できる。
参考
React Native for Webを触ってみた
React Native for Webという言葉はよく耳にしていましたが、具体的にどういうものなのか知らなかったので、触ってみました。
React Native for Web
「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-native
をreact-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が入ってるから動くようになっている。
公式ドキュメントではExpoを推奨してる。
サポートされているコンポーネント
以下のページを見たら良い
React Native compatibility // React Native for Web