はじめに
React Hooks と Context API を利用して Service をどうにかDIっぽく利用してテスト可用性などをあげたいと試行錯誤した結果を簡単な例で紹介します。
(クラス名が適当です)
サービス定義
サービスはクラスの他に Context と useContext する関数を返します。
この際、Context と useContext する関数は {サービス名}Context
use{サービス名}
のように機械的に名前をつけます。
UserService.tsx
export interface User {
name: string;
age: number;
}
export class UserService {
public async findByName(name: string): Promise<User> {
const json = await window.fetch(`/api/user/${name}`)
.then(resp => resp.json());
return json as User;
}
}
export const UserServiceContext = createContext(new UserService());
export function useUserService() {
return useContext(UserServiceContext);
}
サービス利用
サービスを利用する際はuseContextをラップした関数を呼び出します。
User.tsx
export function User(props: { user: string }) {
const userService = useUserService(props.user);
const [user, setUser] = useState<User | undefined>(undefined);
useEffect(() => {
userService.findByName(props.user)
.then(u => setUser(u));
}, [userService, props.user]);
return (
<div>
{user
? `${user.name} is ${user.age} year's old.`
: 'loading...'
}
</div>
);
インジェクション
テストやstorybookなどではServiceをProvideして上書きします。
User.story.tsx
class MockUserService extends UserService {
public async findByName(name: string): {
return { name, age: 42 };
}
}
storiesOf('user', module)
.add('User', () => {
const Wrapped = () => {
return (
<UserServiceContext.Provider value={new MockUserService()}>
<User name="john" />
</UserServiceContext.Provider>
);
};
return <Wrapped />;
});
サービス間の依存
多くのケースでは決まったインスタンス渡しておけばどうにかなりそうな気がします。
UserService.tsx
- export const UserServiceContext(new UserService());
+ export const userService = new UserService();
+ export const UserServiceContext = createContext(userService);
OrgService.tsx
import { userService } from '.../UserService.tsx';
:
export const OrgServiceContext = createContext(
new OrgService(userService),
);
最後に
規模が大きな場合とか複雑なケースではきちんとContextのインターフェースを定義したほうが良いと思いますが、小規模で変更がほぼないのにわざわざ定義するのがめんどうなのです。
おしまい