View/Presenter パターン
Model-View-Presenter の考えに沿って、コンポーネントを View と Presenter の 2つに分離すること
Model のことは一旦忘れる
コードのイメージ
code:components/Counter/index.ts
import { connect } from '../base/connect';
import { useCounterPresenter } from './presenter';
import { CounterView } from './view';
export const Counter = connect(useCounterPresenter, CounterView);
code:components/Counter/presenter.tsx
import { useState, useCallback } from 'react';
import { Presenter } from '../base/connect';
import { CounterViewProps } from './view';
export type CounterProps = {
initialCount: number;
};
export const useCounterPresenter: Presenter<CounterProps, CounterViewProps> = ({ initialCount }) => {
const onIncrement = useCallback(() => setCount((count) => count + 1), []);
const onDecrement = useCallback(() => setCount((count) => count - 1), []);
return { count, onIncrement, onDecrement };
};
code:components/Counter/view.tsx
export type CounterViewProps = {
count: number;
onIncrement: () => void;
onDecrement: () => void;
};
export const CounterView: React.VFC<CounterViewProps> = ({ count, onIncrement, onDecrement }) => {
return (
<div>
<div>羊が… {count}匹</div>
<div>
<button onClick={onDecrement}>-</button>
<button onClick={onIncrement}>+</button>
</div>
</div>
);
};
code:components/base/cnnect.tsx
export type Presenter<P, Q> = (props: P) => Q;
/**
* Presenter と View を接続する.
* @param usePresenter Presenter の役割を持つ Hook。
* @param View View の役割を持つコンポーネント。
*/
export function connect<P extends {}, Q extends {}>(usePresenter: Presenter<P, Q>, View: React.VFC<Q>): React.VFC<P> {
const componentName = View.displayName || View.name || '?';
function ConnectedComponent(props: P) {
const viewProps = usePresenter(props);
return <View {...viewProps} />;
}
ConnectedComponent.displayName = connected(${componentName});
return ConnectedComponent;
}
メリット
ロジックを実装する時は presenter.tsx、デザインコーディングする時は view.tsx を触れば良くなる
分業しやすい
Storybook で扱いやすい
view.tsx を story 化すれば良い
デメリット
ファイルが3つになって扱いづらい
サクッと書けないし、コーディング中もファイルを行ったり来たりすることになって面倒
mizdra.icon まあやろうと思えば1ファイルにも集約できるので、これはどういう規約を敷くかによると思う!