はじめに
Flutterの
useState()や React Hooks の裏側では、
実は「クロージャによる状態保持」が動いています。
この記事では、Dartで自作のuseState的仕組みを作りながら、
関数による状態管理の本質を理解していきます。
ゴール
- クロージャを使って「状態を持つ関数」を作る
- ReactやFlutter Hooksの「useState」を理解する
- GCと状態ライフサイクルを意識した設計を学ぶ
前提:クロージャは“状態を持つ関数”
クロージャとは「関数+定義時のスコープ」をセットで保持する仕組みでした。
Function makeCounter() {
int count = 0;
return () {
count++;
print(count);
};
}
このように「関数なのに状態を持っている」のがクロージャの最大の特徴です。
これを応用すると「関数的状態管理(functional state management)」が実現できます。
useState風の設計をDartで再現
Reactの useState はこんな形ですね:
const [count, setCount] = useState(0);
これを Dartの関数だけで再現 してみましょう。
実装例:useStateを模倣する関数
typedef StateHook<T> = (T Function(), void Function(T));
StateHook<T> useState<T>(T initialValue) {
T state = initialValue;
T get() => state;
void set(T newValue) {
state = newValue;
}
return (get, set);
}
使い方
void main() {
final (count, setCount) = useState(0);
print(count()); // 0
setCount(5);
print(count()); // 5
setCount(count() + 1);
print(count()); // 6
}
解説
-
useStateの内部ではstateという変数がクロージャによってキャプチャされる。 - 関数
get()とset()は、同じスコープのstateを共有する。 - つまり、外部からは見えない「隠れた状態」を保持している。
Flutter Hooksとの対応関係
| React / Flutter Hooks | Dart クロージャ版 |
|---|---|
useState() |
useState<T>() |
get(状態取得) |
count() |
set(状態更新) |
setCount() |
| 状態保持 | クロージャがスコープ内で state を保持 |
| ライフサイクル管理 | GC + スコープ破棄時に自動解放 |
状態更新をトリガーにする構造(useReducer風)
もう少し踏み込んで、「イベントによって状態を更新する」設計にしてみましょう。
typedef Reducer<S, A> = S Function(S, A);
typedef Dispatcher<A> = void Function(A);
typedef StateHookWithReducer<S, A> = (S Function(), Dispatcher<A>);
StateHookWithReducer<S, A> useReducer<S, A>(
Reducer<S, A> reducer,
S initialState,
) {
S state = initialState;
S get() => state;
void dispatch(A action) {
state = reducer(state, action);
}
return (get, dispatch);
}
使い方例:カウンターReducer
void main() {
final (getCount, dispatch) = useReducer<int, String>(
(state, action) {
if (action == 'increment') return state + 1;
if (action == 'reset') return 0;
return state;
},
0,
);
print(getCount()); // 0
dispatch('increment');
print(getCount()); // 1
dispatch('reset');
print(getCount()); // 0
}
解説
-
dispatchがアクションを受け取り、reducerに渡す。 -
reducerは純関数的に状態を計算する。 - 状態は依然としてクロージャに保持され、外部から直接アクセスできない。
これで ReactのuseReducer / Reduxの基本原理 に近い構造をDartだけで再現できました。
メモリ管理とGC
クロージャによる状態保持はヒープ上に変数を置くため、
リストやグローバル変数などに永続参照を残さなければ、
スコープを抜けた時点で自動解放されます。
void demo() {
final (getValue, setValue) = useState(0);
setValue(42);
print(getValue()); // 42
} // ← demo()が終わるとクロージャもGC対象になる
ポイント
- FlutterやReactでは、Widget / Componentのライフサイクルに合わせてクロージャが破棄される。
- Dartではスコープを抜ければ自動で解放される(明示的なdispose不要)。
応用:複数状態を束ねる useStore 的パターン
class Store {
final (getName, setName) = useState<String>('Alice');
final (getAge, setAge) = useState<int>(20);
void printState() {
print('User: ${getName()} (${getAge()})');
}
}
void main() {
final store = Store();
store.printState(); // User: Alice (20)
store.setAge(25);
store.setName('Bob');
store.printState(); // User: Bob (25)
}
これにより、クロージャによる状態管理をクラス内で束ねることができます。
Flutterならこの仕組みを StateNotifier や ViewModel の内部に応用可能です。
まとめ
| 項目 | 内容 |
|---|---|
| 発想の原点 | クロージャ=状態を持つ関数 |
| useStateの仕組み | 変数をキャプチャして保持 |
| 更新方法 | setter / dispatch がクロージャ内の値を更新 |
| メモリ管理 | 参照が切れればGCが自動解放 |
| 応用例 | useReducer / useStore / Stateful関数設計 |
クロージャは「小さなコンテナ付き関数」として、 オブジェクト指向的なクラスを使わずとも、 シンプルな状態保持・更新ロジックを表現できます。
Flutter HooksやReact Hooksの「useState」も、 本質的にはこのクロージャ機構の上に成り立っています。