Dependency Injection
with
React Context

Encapsulation

							<Alert />
<Button />
<Feed />
							
we can take a component like this and use it anywhere we want withouth needing to know anything about its implementation details. makes it easier to read & reason about our code, because all the details that we don't need to worry about right now are hidden away

Customisation

  • Change the colour or text
  • Change the onClick behaviour
  • Change data fetching or state management
  • Change behaviour of child component
  • Change logging/analytics
  • Replace services for testing
we can take a component like this and use it anywhere we want withouth needing to know anything about its implementation details. makes it easier to read & reason about our code, because all the details that we don't need to worry about right now are hidden away
<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

and that's basically all dependency injection is
doesn't quite work with react, because it's referring to instance variables of objects, and react works with functions, change to "giving a component its variables" emphasise a variable could be a value or it could be a function

a 25-dollar term for a
5-cent concept

So why do we need a fancy name like dependency injection if its just props?

    Not all props are dependencies

    <Alert status="error">Something went wrong</Alert>🙅
    <Button type="primary" onClick={...}>Save</Button>🤷
    <Feed 
    	theme="dark" 
    	posts={fetchPublicPosts()} 
    	onLike={showLoginModal}
    	onComment={showLoginModal} 
    	analyticsService={analyticsService} 
    />🎉

Not all dependency injection is props

function Feed() { 
	const { theme, posts, onLike,
		onComment, analyticsService } = Feed.dependencies; 

	// ... 
}

Feed.dependencies = { 
	theme: getTheme(), 
	posts: fetchPublicPosts(), 
	onLike: showLoginModal, 
	onComment: showLoginModal, 
	analyticsService 
}
why would you want to do something like this?
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>
	)
}
					
so we could use a solution like this, to avoid this prop drilling, but it still requires setting the dependencies on each component individually, even if you can skip the "middleman" components
function Post() { 
	// ... 
}
Post.dependencies = { theme: getTheme() }

function LikeButton() { 
	// ... 
}
LikeButton.dependencies = { 
	theme: getTheme(), 
	analyticsService, 
	onLike: showLoginModal 
}
and react gives us a more reacty way to do this

Hooks

so, in our ideal scenario, we could create a container that managed our dependencies, like so
export function App() {
	// ...
	
	return (
		<DependenciesContainer
			dependencies={{ 
				theme: getTheme(), posts: fetchPublicPost(),
				onLike: showLoginModal, onComment: showLoginModal,
				analyticsService
			}}
		>
			<Feed />
		</DependenciesContainer>
	)
}
and then we could use the dependencies in any of the child components something like
function Feed() {
	
	const { 
		theme, 
		posts 
	} = useDependencies();
	
	// ...
}			
function LikeButton() {

	const { 
		theme, 
		onLike, 
		analyticsService 
	} = useDependencies();
	
	// ...
}						
so how do we make this work?
First, we need a function that creates the container component and exposes its value
function createDependencies() {
	const dependencies = { value: undefined }

	return {
		dependencies,
		Container: ({ children, deps }) => {
			dependencies.value = deps
			return children
		},
	}
}
function useDependencies(container) {
	return container.dependencies.value
}		
then we can use our function to create our container
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>
	)
}					
												
and use the hook in our child components
export function LikeButton() {
	const { 
		theme, 
		onLike, 
		analyticsService
	} = useDependencies(feedDependencies)

	// ...
}
squint a little and maybe change some of the variable names, some of this might look a little familiar
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
	} = 

	// ...
}
surprised pikachu meme

React Context
is
Dependency Injection

so that's nice, it's built in, don't need to learn anything, but it's not very discoverable

React Context

  • Built in 👍
  • Discoverability 👎
it('foo', () => {
	render(<Feed />)

	//...
})

libraries

  • Obsidian
  • wox-inject

Obsidian

@Singleton() @Graph()
class ApplicationGraph extends ObjectGraph {
	@Provides()
	httpClient(): HttpClient {
		return new HttpClient();
	}

	@Provides()
	biLogger(httpClient: HttpClient): BiLogger {
		return new BiLogger(httpClient);
	}
}
[docs]
- convert all imports to ES6 classes - make dependencies configurable by making more dependencies

Obsidian

const useButtonClick = ({ biLogger }: Injected): UseButtonPress => {
  const onClick = useCallback(() => {
    biLogger.logButtonClick();
  }, [biLogger]);	
  
  return { onClick };
};

export const useButton = injectHook(
													useButtonClick, 
													ApplicationGraph
												);
dependencies resolved at compile time

This approach helps reduce the amount of boilerplate code required by developers

- The Obsidian Docs

  • solves the discoverability problem 👍
  • difficult to customise 👎
  • test via Jest mocks 😑
@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()
	}
}
  • solves the discoverability problem 👍
  • difficult to customise 👎
  • test via Jest mocks 😑

wox-inject

@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]
similar to obsidian with the annotations, only works with vite, no SSR
  • doesn't solve discoverability 👎
  • customisation? 🤷
  • nice testbed 👍
  • requires other libraries
  • not quite prod-ready? 🤷
@Injectable()
class Posts {
	onLike(postId: string) {
		return doLike(postId);
	}
}
	
function LikeButton() {
	const handleLike = useResolve(Posts);
	
	return (
		<button onClick={handleLike}>🩶</button>
	);
}
  • doesn't solve discoverability 👎
  • customisation? 🤷
  • nice testbed 👍
  • requires other libraries 😑
  • not quite prod-ready? 🤷
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;

	// ...
}

What about dependencies that aren't exposed?

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)
})

react-magnetic-di

one library i do find super useful though, specifically for injecting dependencies in tests no specific set-up, code doesn't even know that its dependencies are being injected can replace basically anything that's imported examples
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)
  • Build time (Babel)
  • Can replace any export
  • Best with classes & functions
  • No library-specific code
can be done without babel, but is a bit trickier if you want to inject a value, turn it into a getter function

Summary

  • Customisation + Encapsulation
  • React context == DI
  • createContext(undefined)
  • react-magnetic-di

Thank you

di.ez.codes