Edited at

Fleur という Flux フレームワークが良い感じ

Pixiv のエンジニアの方が作った Fleur という Flux フレームワークを触ってみて、とても良いなという感触を得たので紹介したいと思います。

ra-gg/fleur: A new fully-typed Flux Framework inspired by Fluxible


Fleur とは

【SSRも】Fleur − 新しいFluxフレームワーク【イケる】

Fleur がどんなフレームワークか、というのは作者の方がブログで詳しく書いているので、この記事では実際に書いてみたコードと使い心地に焦点を当てます。

かいつまんで紹介すると、


  • Fluxible を参考にしている

  • SSR に対応している

  • React Hooks に対応している

  • 非同期処理に対応している

  • Store は immer.js を組み込んでいる

  • SSR に対応したルーターを持っている

こんな感じで、モダンな作りと幅広い対応範囲が特徴的です。

詳しい数値はブログの方に書いてありますが、パフォーマンス面も Redux + react-redux の性能に迫るようで、 Pixiv 内のプロダクトで実際に使われているだけあってしっかりと作り込まれています。

作者の方からコメントをいただき、 Pixiv 内のプロダクトで使われているわけではなく、個人開発のプロダクト内の誤りでした。ただ実際に動いているプロダクト内でがっつり使われていることは間違いないです:bow:(19/05/27 18:03追記)

image.png

ブログ内で紹介されている画像を使わせていただきました。

各ドメインごとに Actions, Operations, Store を定義して Store をコンポーネントに繋げる仕組みになっています。

4つの要素を作るということでがっつり構造を分けることになるのですが、定義に必要な記述は少なく、 Redux のように多くの記述を必要とせずに作ることができます。


OXゲームを作ってみた

React の公式チュートリアルでは、OXゲームを作るという課題を与えられるのですが、今回はそれを Fleur を使ってやってみました。

中大規模アプリ向けと言及されていたので、正直このレベルのゲームを作る程度だと恩恵を受けるのが難しいところではあるのですが、書き心地を確認するという意味ではまあいいかなと思います。

Fleur の特徴である SSR や非同期処理やルーターなどの機能は使用していないのですが、まともなアプリケーションを作った際にまたそれらの機能の感想などは書ければなと。

実際に作ったゲームのソースコードはこちらに置いています。

nabeliwo/fleur-tic-tac-toe

作っているゲームの機能などはチュートリアルの方を見ていただければ。

最近 React の公式が日本語化されたので一度見てみることをオススメします!


Actions

まずは Action を定義します。

マス目をクリックした際に発行される、 setMark と履歴一覧のうちの好きなところに戻る jumpTo という2種類の Action を定義しています。


gameActions.ts

import { actions, action } from "@fleur/fleur";

import { Mark } from "./gameStore";

export const GameActions = actions("GameAction", {
setMark: action<{ order: number; mark: Mark }>(),
jumpTo: action<{ step: number }>()
});



Operations

次に Operation を定義します。

ここに非同期処理を書くことができるので async を使用していますが、今回は非同期処理を必要としないためシンプルに Action を dispatch しているだけです。


gameOperations.ts

import { operations } from "@fleur/fleur";

import { GameActions } from "./gameActions";
import { Mark } from "./gameStore";

export const GameOps = operations({
async setMark({ dispatch }, order: number, mark: Mark) {
dispatch(GameActions.setMark, { order, mark });
},

async jumpTo({ dispatch }, step: number) {
dispatch(GameActions.jumpTo, { step });
}
});



Store

次に Store を作ります。

ここで state を管理して dispatch した Action を受けて state を更新します。

updateWith で state を更新することができるのですが、 Redux 思考に慣れてしまった自分は新しい state を返すのではなく state を直接書き換えていることに最初「うっ…」となったのですが、実際にはこちらの方が記述が見やすくて良いですね。

特に配列の中身変更するような場面だと Redux の reducer だと配列回して該当部分だけ変更するみたいな記述になるのでちょっと見辛くなりがちでした。

ここにロジックを押し込んだので Store が少し大きくなっています。


gameStore.ts

import { listen, Store } from "@fleur/fleur";

import { GameActions } from "./gameActions";

export type Mark = "O" | "X";
export type Matrix = (Mark | null)[][];
type State = {
history: Array<{ squares: Array<Mark | null> }>;
xIsNext: boolean;
step: number;
};

export class GameStore extends Store {
static storeName = "GameStore";

public state: State = {
xIsNext: true,
history: [{ squares: Array(9).fill(null) }],
step: 0
};

public get allHistory() {
return this.state.history;
}

public get currentHistory() {
const { history, step } = this.state;
return history.slice(0, step + 1);
}

public get currenMatrix() {
const squares = this.getCurrentSquares();
return [squares.slice(0, 3), squares.slice(3, 6), squares.slice(6, 9)];
}

public get nextMark() {
const nextMark: Mark = this.state.xIsNext ? "X" : "O";
return nextMark;
}

public get winner() {
const lines = [
[0, 1, 2],
[3, 4, 5],
[6, 7, 8],
[0, 3, 6],
[1, 4, 7],
[2, 5, 8],
[0, 4, 8],
[2, 4, 6]
];
const squares = this.getCurrentSquares();

for (let i = 0; i < lines.length; i++) {
const [a, b, c] = lines[i];

if (
squares[a] &&
squares[a] === squares[b] &&
squares[a] === squares[c]
) {
return squares[a];
}
}

return null;
}

private getCurrentSquares = () => {
const currentHistory = this.currentHistory;
const { squares } = currentHistory[currentHistory.length - 1];
return squares;
};

private handleSetMark = listen(GameActions.setMark, ({ order, mark }) => {
if (this.winner) return;

this.updateWith((draft: State) => {
const currentHistory = this.currentHistory;
const squares = this.getCurrentSquares().slice();
squares[order] = mark;

draft.history = currentHistory.concat([{ squares }]);
draft.xIsNext = !draft.xIsNext;
draft.step = currentHistory.length + 1;
});
});

private handleJumpTo = listen(GameActions.jumpTo, ({ step }) => {
this.updateWith((draft: State) => {
draft.xIsNext = step % 2 === 0;
draft.step = step;
});
});
}


まだ Fleur のソースコードを読んでいないので handleSetMarkhandleJumpTo がどのように実行されるのかがわかっていないのですが、定義することで Action を listen します。

このクラスの中で storeName を定義しないとエラーになってしまったので現状とりあえず書いているのですが、もうちょっと調べてみて解決したらこの記事を編集したいと思います。

作者の方からコメントをいただき、現状 Fleur 内部で storeName を使って管理をしているため storeName の定義は必須ということでした(19/05/27 18:07追記)


View

次に Store をコンポーネントに繋ぎます。

Store から state を呼び出したり、 Operations を呼び出したりします。この繋ぎ込みを行うのを Redux で言う Container に当たる部分として考えて Component と切り分けると綺麗に分割できそう。


Game.tsx

import React, { useCallback } from "react";

import { useFleurContext, useStore } from "@fleur/fleur-react";

import { GameOps } from "../../domains/game/gameOperations";
import { GameStore } from "../../domains/game/gameStore";
import { Board } from "../Board";
import { Step } from "../Step";
import "./style.css";

export const Game = () => {
const { allHistory, matrix, nextMark, winner } = useStore(
[GameStore],
getStore => {
const store = getStore(GameStore);
return {
allHistory: store.allHistory,
matrix: store.currenMatrix,
nextMark: store.nextMark,
winner: store.winner
};
}
);
const { executeOperation } = useFleurContext();
const handleClickSquare = useCallback(
(order: number) => executeOperation(GameOps.setMark, order, nextMark),
[executeOperation, nextMark]
);
const handleClickStep = useCallback(
(step: number) => executeOperation(GameOps.jumpTo, step),
[executeOperation]
);

return (
<div className="game">
<div className="game-board">
<Board matrix={matrix} onClick={handleClickSquare} />
</div>

<div className="game-info">
<div>{winner ? `Winner: ${winner}` : `Next player: ${nextMark}`}</div>
<ol>
{allHistory.map((step, move) => {
return (
<li key={move}>
<Step move={move} onClick={handleClickStep} />
</li>
);
})}
</ol>
</div>
</div>
);
};


useFleurContext と useStore が @fleur/fleru-react の機能です。

定義した GameStore を useStore に渡すことで state を抽出することができます。

useFleurContext から executeOperation を取り出して Operation を実行することができます。

最後にアプリケーションのルートで Fleur を繋ぎ込みます。


index.tsx

import React from "react";

import ReactDOM from "react-dom";
import Fleur from "@fleur/fleur";
import { FleurContext } from "@fleur/fleur-react";

import { GameStore } from "./domains/game/gameStore";
import { Game } from "./components/Game";
import "./index.css";

const app = new Fleur({
stores: [GameStore]
});
const context = app.createContext();

window.addEventListener("DOMContentLoaded", () => {
const root = document.querySelector("#root");

ReactDOM.render(
<FleurContext value={context}>
<Game />
</FleurContext>,
root
);
});


new Fleur に定義した Store を渡すことで子孫コンポーネントで該当 Store を取り出すことができるようになっています。

Context API をうまく使っていて良い感じ。

その他のコンポーネントは特に言うことはないので GitHub のリポジトリを見て確認していただければと思います。


まとめ

簡単なゲームを作ってみての感想として、とにかく書きやすいです。

最初ブログを見たときにフレームワークの特徴として「コードの書き心地が良い」というのがあって、「主観では・・・ :thinking: 」となったのですが、書いてみると確かにこれまでやってきた Flux フレームワークと比べると個人的にとても書きやすく、少なくとも僕の書き心地の感覚とは一致しているようでした。

ブログの方でオススメのディレクトリ構成などについても詳しく記述されていて、今回のOXゲームを作るにあたって、一切手が止まることなくスラスラと書けました。

ここ最近の流れとして大事な部分である SSR やルーター部分の感想が言えないのが紹介記事としては不十分ではあると思うのですが、そちらはまた個人開発のアプリケーションで使ってみて何かあれば追記しようと思います。

React 公式の方で SSR の対応が進んでいたり、 Google のクローラーが CSR を評価できるようになってきたりなど、フレームワーク側での対応が必要じゃなくなりつつあるかもしれない昨今ではありますが、現状ではやはり toC のアプリケーションを作る上では対応しなければならない問題だと思うのでやはりフレームワーク側で担保してくれるのは非常に助かります。

リリースしたばかりでまだまだ発展途上とのことですが、十分選択肢の一つとして考えることができるフレームワークかと思いますので、 Fleur をオススメさせていただきます。