<Alert />
<Button />
<Feed />
<Alert status="error">Something went wrong</Alert>
<Button type="primary" onClick={...}>Save</Button>
<Feed
theme="dark"
posts={fetchPublicPosts()}
onLike={showLoginModal}
onComment={showLoginModal}
analyticsService={analyticsService}
/>
Dependency injection means
giving an object a component
its instance local variables.
- James Shore,
Dependency Injection Demystified
a 25-dollar term for a
5-cent concept
<Alert status="error">Something went wrong</Alert>
🙅
<Button type="primary" onClick={...}>Save</Button>
🤷
<Feed
theme="dark"
posts={fetchPublicPosts()}
onLike={showLoginModal}
onComment={showLoginModal}
analyticsService={analyticsService}
/>
🎉
function Feed() {
const { theme, posts, onLike,
onComment, analyticsService } = Feed.dependencies;
// ...
}
Feed.dependencies = {
theme: getTheme(),
posts: fetchPublicPosts(),
onLike: showLoginModal,
onComment: showLoginModal,
analyticsService
}
function Feed({ theme, posts, onLike,
onComment, analyticsService }) {
return (
<FeedContainer theme={theme}>
{posts.map(post => (
<Post
post={post} theme={theme}
onLike={onLike} onComment={onComment}
analyticsService={analyticsService}
/>))}
</FeedContainer>
)
}
function Post({ theme, onLike, onComment,
analyticsService, post }) {
return (
<PostContainer theme={theme}>
<Content theme={theme} post={post} />
<LikeButton
theme={theme} onLike={onLike}
analyticsService={analyticsService} />
<CommentButton
theme={theme} onLike={onLike}
analyticsService={analyticsService} />
</PostContainer>
)
}
function Post() {
// ...
}
Post.dependencies = { theme: getTheme() }
function LikeButton() {
// ...
}
LikeButton.dependencies = {
theme: getTheme(),
analyticsService,
onLike: showLoginModal
}
export function App() {
// ...
return (
<DependenciesContainer
dependencies={{
theme: getTheme(), posts: fetchPublicPost(),
onLike: showLoginModal, onComment: showLoginModal,
analyticsService
}}
>
<Feed />
</DependenciesContainer>
)
}
function Feed() {
const {
theme,
posts
} = useDependencies();
// ...
}
function LikeButton() {
const {
theme,
onLike,
analyticsService
} = useDependencies();
// ...
}
function createDependencies() {
const dependencies = { value: undefined }
return {
dependencies,
Container: ({ children, deps }) => {
dependencies.value = deps
return children
},
}
}
function useDependencies(container) {
return container.dependencies.value
}
const feedDependencies = createDependencies()
export function PublicFeed() {
return (
<feedDependencies.Container
deps={{
theme: getTheme(),
posts: fetchPublicPosts(),
onLike: showLoginModal,
onComment: showLoginModal,
analyticsService
}}
>
<Feed />
</myDependencies.Container>
)
}
export function ChronologicalFeed() {
return (
<feedDependencies.Container
deps={{
theme: getTheme(),
posts: fetchUserPosts(),
onLike: postLike,
onComment: postComment,
analyticsService
}}
>
<Feed />
</myDependencies.Container>
)
}
export function LikeButton() {
const {
theme,
onLike,
analyticsService
} = useDependencies(feedDependencies)
// ...
}
createDependencies();
createContext();
const feedDependencies =
feedDependencies.Container
feedDependencies.Provider
feedDependencies.Container
>
feedDependencies.Provider
>
export function PublicFeed() {
return (
<
value={{
theme: getTheme(),
posts: fetchPublicPosts(),
onLike: showLoginModal,
onComment: showLoginModal,
analyticsService
}}
>
<Feed />
</
)
}
useDependencies(feedDependencies)
useContext(feedDependencies)
export function LikeButton() {
const {
theme,
onLike,
analyticsService
} =
// ...
}
it('foo', () => {
render(<Feed />)
//...
})
@Singleton() @Graph()
class ApplicationGraph extends ObjectGraph {
@Provides()
httpClient(): HttpClient {
return new HttpClient();
}
@Provides()
biLogger(httpClient: HttpClient): BiLogger {
return new BiLogger(httpClient);
}
}
[docs]
const useButtonClick = ({ biLogger }: Injected): UseButtonPress => {
const onClick = useCallback(() => {
biLogger.logButtonClick();
}, [biLogger]);
return { onClick };
};
export const useButton = injectHook(
useButtonClick,
ApplicationGraph
);
This approach helps reduce the amount of boilerplate code required by developers
@Singleton() @Graph()
class Feed extends ObjectGraph {
@Provides()
publicPosts(): Post[] {
return new PublicPosts();
}
@Provides()
userPosts(): Post[] {
return new BiLogger(httpClient);
}
@Provides()
appConfig(): AppConfig {
return new AppConfig();
}
@Provides()
posts(): Post[] {
return appConfig.algorithm === user ?
publicPosts() :
userPosts()
}
}
@Injectable()
class GreeterService {
greet(val: string) {
console.log(val);
}
}
function App() {
const greeterService = useResolve(GreeterService);
return (
<button
type="button"
onClick={
() => greeterService.greet('hello!')
}
>
click me
</button>
);
}
[docs]
@Injectable()
class Posts {
onLike(postId: string) {
return doLike(postId);
}
}
function LikeButton() {
const handleLike = useResolve(Posts);
return (
<button onClick={handleLike}>🩶</button>
);
}
const feedDependencies = createContext({ theme: getTheme(), getPosts: fetchPublicPosts, onLike: showLoginModal, onComment: showLoginModal, analyticsService });
❌
const feedDependencies = createContext(undefined)
function LikeButton() {
const dependencies = useContext(feedDependencies);
if (!dependencies) {
throw new Error(
'LikeButton requires DependenciesContext'
)
}
const {
theme,
onLike,
analyticsService
} = dependencies;
// ...
}
function LoginForm() {
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const onSubmit = () => {
if (username && password) {
login(username, password)
}
}
return (
<form onSubmit={onSubmit}>
<input value={username} onChange={setUsername} />
<input type="password" value={password} onChange={setPassword} />
</form>
)
}
it("calls the login service", () => {
render(<LoginForm />)
// fill in form
expect(login).toHaveBeenCalledWith(username, password)
})
import {
injectable, DiProvider
} from "react-magnetic-di";
import { login } from './services'
it("calls the login service", () => {
const loginDi = injectable(login, jest.fn())
render(<LoginForm />, {
wrapper: (props) =>
<DiProvider use={[loginDi]} {...props} />,
})
// fill in form
expect(loginDi).toHaveBeenCalledWith(username, password)
})
function LoginForm() {
//...
const onSubmit = () => {
if (username && password) {
login(username, password)
}
}
//...
}
function LoginForm() {
//...
const onSubmit = () => {
if (username && password) {
const loginDi = _di(login)
loginDi(username, password)
}
}
//...
}
const loginDi = injectable(login, jest.fn())
const loginDi = _di(login)
loginDi(username, password)
createContext(undefined)