本記事では、2024年11月時点におけるFlutterとReact ecosystemについて、開発言語の性質とコンポーネントの指向性を中心に比較を行う。
FlutterおよびReact ecosystemとは何か?
本記事では、Reactおよびその関連プラットフォーム、フレームワーク、パッケージをまとめてReact ecosystemと呼ぶ。
Table 1. FlutterとReact ecosystemの一部
ツール | 言語 | 位置づけ | 対応プラットフォーム |
---|---|---|---|
Flutter | Dart | フレームワーク | Windows, macOS, Linux, Web, Android, iOS |
React | TypeScript | 言語パッケージ | Web |
React Native | TypeScript | プラットフォーム | Android, iOS |
JavaScript
動的型付け
JavaScript(Js)は、フロントエンド向けに開発されたプログラミング言語である。現時点でウェブサイトのクライアントサイドは、98.9%がJavaScriptで記述されている。従って、DartやTypeScript(Ts)を含むほぼすべてのフロントエンド開発言語はJsにコンパイルされる。本記事は、JsのスーパーセットであるTsおよび新規開発されたDartを扱う。
TypeScript
静的型付け
TypeScriptはJsの型チェッカーとして開発された言語である。この為、TsはJsのスーパーセットとして型付け等を実装している。Tsの目的は、Jsによる大規模開発の支援であり、静的型付けは、Jsにおける実行時エラーの一部をコンパイルエラーとして検知できるような仕組みとして導入されている[Ref]。以下はTs向けのパッケージやプラットフォームの一例であり、多くが互いに補完しあう関係にある。また、JsはHTMLやCSSとの併用を想定して設計されているため、Js系フレームワークの一部はこれら言語もサポートする。
・言語パッケージ+フレームワーク
React
+ Next.js
= Web
Vue.js
+ Nuxt.js
= Web
・フレームワーク
Electron
= Windows, macOS, Linux
Tauri
= Windows, macOS, Linux, Android, iOS
・プラットフォーム
Angular
= web
React
ReactはJsのパッケージとして開発されているが、本記事ではTSXを利用した記述を前提とする。
TSXの前提となるJSX (XML-like syntax extension)は、MetaによるJsの拡張記法であり以下のような記述をサポートする。
//test.jsx
function MyButton() {
return (
<button>I'm a button</button>
);
}
const templateText="Hello world"
export default function MyApp() {
return (
<div>
<h1>{templateText}</h1>
<MyButton />
</div>
);
}
独自のXMLタグ定義と{...}による埋め込みJs記法の実装が主なJSXの拡張であり、動的なコンポーネント制御をサポートする。
TSXはJSXのTs拡張であり、単に型を追加して以下のように書ける。
//test.tsx
const templateText: String = "Hello world"
Interface
またはtype
キーワードを利用することで、独自型も定義可能である。
interface User {
id: number;
name: string;
readonly index:number;
}
type Point = {
x: number;
y: number;
};
interfaceはextendsを、typeはユニオン型の記述を許可する。
主観的意見
JSX記法が直接のHTML拡張ではなくXML-likeに設計されたのは、マークアップ言語にJsを埋め込むための安全なスコープ管理が背景にあるように思える。
Dart
静的型付け
Dartとは、クライアントサイドに最適化された任意のプラットフォーム向け開発言語であり、複数プラットフォーム開発に対して最も生産的な言語の提供を目的としている[Ref]。Dart VMの初期構想では、Chromeへの統合が期待されていたが、2015年を機にこれをしないと決定したようである[Ref]。
Flutter
Flutterとは、マルチプラットフォームアプリケーションを単一コードベースで記述する為のフレームワークである[Ref]。
文法は、Dartと完全に統合されており、コンポーネントに関する思想はReactにインスパイアされたことが明記されている[Ref]。以下はFlutterの簡単な例である。
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
Dart vs TypeScript
Table 2.はDartおよびTsの定量的指標をまとめたものである。
以降はだいたい主観で書かれている。客観性を重要視するなら、Flutter vs Reactにスキップすることを推奨する。
Table 2. 各言語の定量的特性
言語 | 予約語(型を除く) | 言語の人気率(2024/11) | 言語の登場時期 |
---|---|---|---|
TypeScript | 46個 | 0.19% (Js: 3.71%) | 2012/10/1 |
Dart3.5 | 69個 | 0.53% | 1995/12/4 |
TypeScriptとTSXの気に食わないところ
1. ダサい記法
式は返り値があるし、文は定義時にのみvoid型宣言するのだから、functionキーワードはいらない。例えば、以下のようなTsの関数は、
//
function add(x: number, y: number): number {
return x + y;
}
const add = (x: number, y: number): number => {
return x + y;
};
と書くより、
number add(x: number, y:number){
return x+y;
}
number add(x: number, y:number)=>{
return x+y;
};
の方が、キーワードが少ない。アロー構文のコンテクストが定義時のスコープに依存するのに、functionのスコープが実行時に決まるのも気に食わない。依存性があるなら注入するべきだし、型チェックを重んじるなら適当なthisを実行時に参照するような仕組みは廃止するべきだと思う。ついでに、関数を実行時に書き替えるような設計はそもそも許可するべきじゃないから、const
キーワードは省略するべき。実行次に値の決まるconst
なんて最低だと思う。コンパイル時に決定するRustのconst fn
を良く見て欲しい。
2. オプショナルなセミコロン
消して欲しい。それか文と式で明確にしてほしい。式でreturn
するRustが嫌な目でこちらを見ている。
const value=10;//セミコロンあり
const item="name"//セミコロンなし
いらないなら消してくれ。
3. === != ==
Loose Equality(==)とStrict Equality(===)。
静的型付けを志すのに、1=="1"
がtrue
なのは辞めてください。
Loose/Strictは等価性について何の情報も与えていません。詳細は言葉を濁さない。
合わせて読む:Loose Equalityの実態
4. CSSs
Cssにはいろんな記法があるらしい。Claudeに出力してもらった(ちゃんと確認してないです)。
ちなみに文字列リテラルベースでオートフィルやサジェストが働かないかない奴は全部嫌い。HTMLのセマンティクスを捨てるなら、表示内容をUIの一部として単一責任のもとでJsが扱うべき。コンポーネントのデザインは、コンポーネントのプロパティであるべきだし、有限状態のプロパティはクラスとして事前定義されているべき。そうすれば、"blue"
ではなく、Colors.blue
としてオートフィルが働くし、grey
とgray
がタイポなのか文化の違いかで悩む必要もなくなる。
CSS Modules
// styles.module.css
.container {
background: #fff;
padding: 20px;
}
// Component.tsx
import styles from './styles.module.css';
const Component = () => {
return <div className={styles.container}>...</div>;
};
CSS-in-JS
import styled from 'styled-components';
const Button = styled.button`
background: ${props => props.primary ? "blue" : "white"};
color: ${props => props.primary ? "white" : "black"};
padding: 10px 20px;
`;
const Component = () => {
return <Button primary>Click me</Button>;
};
Emotion
/** @jsx jsx */
import { css, jsx } from '@emotion/react';
const style = css`
color: red;
padding: 20px;
`;
const Component = () => {
return <div css={style}>Content</div>;
};
Inline Styles
const styles = {
container: {
backgroundColor: '#fff',
padding: '20px',
} as React.CSSProperties
};
const Component = () => {
return <div style={styles.container}>...</div>;
};
Tailwind CSS
const Component = () => {
return (
<div className="bg-white p-4 rounded-lg shadow">
Content
</div>
);
};
CSS Custom Properties
// styles.css
:root {
--primary-color: #007bff;
}
// Component.tsx
const Component = () => {
return (
<div style={{ color: 'var(--primary-color)' }}>
Content
</div>
);
};
5. {{}}
なぜ文句を言わないのか?JSXの中に辞書を直接埋め込めるの事は幸福か?
return (
<div style={{ color: "white" }}>
Content
</div>
);
6. JSX内部でのコメントアウトは手間が多い。
return (
<div style={//{ color: "white" }}> この辺までコメントアウトされるから、改行が必要。
Content
</div>
);
return (
<div style={//{ color: "white" }
}> //ちなみにこれはコメントアウトじゃない。あとこんな書き方普通はしない。
Content
</div>
);
7. <><div></div></>
もうこの辺は理解してない。
Dartの気に食わないところ
特にない。そう、これはそういう趣旨の記事である。
FlutterとReactの差異
ここでは、FlutterとReactの差異についてショーケース、パッケージ、UIコンポーネントの記述法を主題に述べる。
パッケージ
Jsのパッケージが利用可能なReactは、Flutterに比べて大きいパッケージ資産を持つと考えられる。
React自体がパッケージとしてJsの資産を併用する一方で、フレームワークであるFlutterにはFlutter Favoriteとして特定の条件が担保されたサードパーティー製パッケージ群が示されている。
Reactの優位性をもつ一例では、jsには良く知られたWeb向け3DパッケージであるThree.jsが存在する一方で、少なくともFlutter Favoriteに3Dライブラリは見つけられない。
基幹的なパッケージは両者で良く発達しており、Flutterでは状態管理のriverpod
やSQLiteのFlutter向けバインディングであるsqfliteが広く利用されている。
ショーケース
ReactおよびFlutterはそれぞれMetaとGoogleが開発母体である。ここでは、React Nativeを含めて、いくつかのショーケースを紹介する。
画像はそれぞれショーケースのURLとWikiより引用。
React (web)
Paypal
Netflix
Shopify
Facebook
React Native (mobile)
Instagram
AmazonShopping
Flutter (multi platform)
Google Earth
UIコンポーネントの記述法
主観的です
言語とパッケージ毎の特性を除けば、両者の違いはそのUIコンポーネントの記述法にある。
Flutterのコンポーネントはオブジェクティブな記法をサポートし、Reactの記法はリテラルベースでセマンティクスを維持する構造になっている。
例えば、Flutterの標準的なコンポーネント単位であるContainer
クラスは、以下のように記述できる。
Center(
child: Container(
margin: const EdgeInsets.all(10.0),
color: Colors.red,
width: 48.0,
height: 48.0,
child: Text("Hello world")
),
)
Center
に始まるスニペットは全体でセマンティクスを持たず、ただUIの構造を記述している。
つまり、"Hello world"が何段落であるとかその種の事に一切関心がない。
一方で、Reactの標準的なコンポーネントは以下のように書かれる。
function Video({ video }) {
return (
<div>
<Thumbnail video={video} />
<a href={video.url}>
<h3>{video.title}</h3>
<p>{video.description}</p>
</a>
<LikeButton video={video} />
</div>
);
}
この例では、<h3>
や<p>
が段落や改行といったセマンティックな意味を持っている。ちなみに、この代入されるvideoはCSSかTsのコンポーネントとして書かれている。
そして、たぶんリテラルベースの実装である。grey
とかgray
とかである。
まとめ
FlutterはUIをデザインとして評価し、Reactは構造化された情報とその修飾という立場を取っている様に見える。だからこそ、FlutterはContainer
クラスを実装し、Reactには<h>
タグが存在する。既存のWeb技術者にとって、Reactは自然なモダナイズに見えると思うし、要求を十全に満たす良い選択肢であることは否定できない。一方で、オブジェクティブで汎用的を重んじる人々にとっては、HTMLのセマンティックスがもたらす忌避感は大きいと思う。ReactのJSXは組版的な設計であるからWordやMarkdownに近いし、Reactのスニペットはそれ自体がUIにおける居場所をある程度示してくれる。Flutterが示すContainer
の居場所は必ず相対的に決定されるし、組版よりは汎用的なデザインパターンとして設計されている。
実際には<h100>
タグの必要な文書はめったにないし、UIは常に意味を持っているからセマンティックな設計でもいいかもしれない。
ただし、Webが単なるデータ表示プラットフォームであった時代は終わっている。
そして、私は恣意的な設計が嫌いである。
<h100>
を許容しないなら<h3>
も許容するべきではない。
資産よりも理想的な設計を追求するべきだと思う。
仮に難しくとも、理想的な道はきっと楽しい。