unstated-nextとredux-actionsを組み合わせると素敵

github.com

unstated-nextは、Reactの新しいContext APIの極薄のラッパー。ソースコードもたった38行(空行込み)。

github.com

redux-actionsは、FLUXスタイルのアクションを作る極薄ライブラリ。名前にReduxが入ってるのでRedux専用かというと、そうではない。元々Reduxも普通のJavaScript(/TypeScript)関数を組み合わせる仕組みなのでその便利ライブラリたるredux-actionsも普通の関数を作る。

これら二つは、React Hookの一つReact.useReducerで組み合わせる。

ja.reactjs.org

組み合わせると、こんな感じのコードができました。

import React from 'react';
import { createContainer } from 'unstated-next';
import { ActionFunctions, createAction, handleAction, Reducer } from 'redux-actions';
import * as R from 'ramda';

import { ConcreteState } from './types';

type State = ConcreteState;

interface ReducerFactory<Payload> {
    (initialState: State): Reducer<State, Payload>;
}

// あまり美しくない。単にカリー化したいだけなのにR.curry(handleAction)(a, r)ではPayloadの型を失う。
const createReducer = <Payload = void>(
    actionType: ActionFunctions<Payload>,
    reducer: Reducer<State, Payload>,
): ReducerFactory<Payload> => (initialState: State) => handleAction(
    actionType,
    reducer,
    initialState,
);

const useContainer = (reducers: ReducerFactory<any>[]) => (initialState?: State) => {
    if (!initialState) {
        throw new Error('"initialState" is undefined.');
    }
    // 前にやったことと同じ see: http://mk.hatenablog.com/entry/2017/09/09/070852
    const combineded = R.reduce<Reducer<State, any>, Reducer<State, any>>(
        (pre, cur) => (state, action) => cur(pre(state, action), action),
        state => state,
        R.map(r => r(initialState), reducers), // ReducerFactoryの配列からReducerの配列を作る
    );
    // [ State, React.Dispach<Action<any>> ] というTupleを返す。これをアプリから利用する。
    return React.useReducer(combineded, initialState);
}

// ——— Here we go! ———

// redux-actionsでFLUXスタイルのActionを作る
const doSomethingAction = createAction<number>('DO_SOMETHING');
const doSomethingReducer = createReducer(
    doSomethingAction,
    (state, { payload }) => {
         // (具体的な操作は省略) payloadがしっかり型付けできている。
        return state;
    },
);

const doAnotherAction = createAction<boolean>('DO_ANOTHER');
const doAnotherReducer = createReducer(
    doAnotherAction,
    (state, { payload }) => {
        // (具体的な操作は省略) Ramdaを活用してスマートにstateを処理する
        return state;
    },
);

// importが楽なようにActionをまとめた
export const actions = {
    doSomethingAction,
    doAnotherAction,
}

// unstated-nextのReactコンポーネントファクトリでコンテナを作る
export default createContainer(useContainer([
    doSomethingReducer,
    doAnotherReducer,
    // あとはパターンを利用して量産したReducerFactoryを追加していく。
]));

すごく素敵。さらにStateをいじるのにこれまではImmutable.jsとかunderscore.jsを使ってたけど、ここ一年ちょっとぐらいはめちゃ関数型プログラミングないいやつ愛用中。作りがImmutable.jsのように副作用ないのは同じながらImmutable.jsがコレクションを提供していたのでアプリケーション丸ごと染まるのに対して、underscoreのようなコレクション操作等での提供なので、使い始めたいところだけ部分でも使える。ライブラリ全てがカリー化に対応している。

github.com

ここまで全て関数型プログラミングなライブラリ群で、当然ReactもHook使ったFunctionConponentでゴリ押しすると美しい。React.useEffectがあるのでライフサイクル必要でもClassComponentを使う必要なくなった。

そして、UIの見てくれ的なものはこれまでも今もMaterial-UIが良かったのだけど、ここ数日は違うのに浮気中。

github.com

Semantic-UIはCSSライブラリで、JSXのclassName属性へ直書きするだけで十分に使える。さらにSemantic-UI-ReactというReactコンポーネントも用意されていてそれはそれで便利。ただしMaterial-UIのようながっつりコンポーネントで作り込んでるのではないのでCSSの範囲の効果にほぼ限る。複雑なものはMaterial-UIのソース見てだいたい仕組み把握したら、Semantic-UI-Reactのコンポーネントを組み合わせながらSemantic-UIのCSSを使えば結構簡単に良い感じのものができる。