eyeosee v1.0.0
ποΈ EyeOSee
Simplify dependency injection in React with ease and type safety.
π Table of Contents
- β¨ Why EyeOSee?
- π Features
- π¦ Installation
- π Getting Started
- βοΈ Core Concepts
- π‘ Usage Examples
- π§ͺ Testing
- π Conclusion
- π Contributing
- π License
β¨ Why EyeOSee?
Imagine building a React app where your components, hooks, and helpers are seamlessly organized, effortlessly tested, and elegantly managed. EyeOSee makes this vision a reality by introducing a simple, type-safe dependency injection system for React projects.
Without EyeOSee, managing dependencies in a React app can become a tangled web:
- Components tightly coupled to hooks and utilities.
- Hard-to-mock dependencies, making unit testing a nightmare.
- Growing codebases that become difficult to maintain.
EyeOSee solves this by providing a smooth, automated way to inject dependencies, keeping your code modular and testable.
π Features
- π Effortless Dependency Injection - Simplify how your React components, hooks, and helpers connect.
- π§© Seamless Separation of Concerns - Cleanly split logic for better maintainability.
- π TypeScript Native - Fully type-safe for confident coding.
- π§ͺ Testing Made Simple - Isolate and test each piece independently.
π¦ Installation
Install EyeOSee using your favorite package manager:
npm install eyeosee
# or
yarn add eyeosee
π Getting Started
1οΈβ£ Generate the Container
Your application's dependencies live in a container. Generate it easily:
Using CLI:
npx eyeosee-generator generate
π This will create a file named eyeosee-container.gen.ts
in your src
directory.
Using Bundler Plugins:
Keep your container updated automatically using the EyeOSee bundler plugins.
For Vite:
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { eyeoseeVitePlugin } from "eyeosee/bundler";
export default defineConfig({
plugins: [
react(),
eyeoseeVitePlugin({
// Source files to generate the container from
includes: ["src/**/*.ts", "src/**/*.tsx"],
}),
],
});
For Webpack:
const path = require("path");
const { eyeoseeWebpackPlugin } = require("eyeosee/bundler");
module.exports = {
entry: "./src/index.jsx",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
plugins: [
new eyeoseeWebpackPlugin({
// Source files to generate the container from
includes: ["src/**/*.ts", "src/**/*.tsx"],
}),
],
};
β οΈ Note: Webpack requires restarting the dev server when adding new files.
βοΈ Core Concepts
At the heart of EyeOSee is its container system, designed to manage and inject dependencies seamlessly. Here's a detailed breakdown of its core components:
ποΈ The container
container
β The central instance that holds all registered dependencies. Think of it as a smart registry for your app's logic.ContainerDependencies
β A TypeScript type that maps all registered dependencies in the container. This provides full type safety and ensures consistent use of dependencies across your project.
π Registration functions
EyeOSee provides intuitive functions to register different kinds of dependencies:
registerConfig
β Registers parameters meant to be injected.registerFunction
β Registers utility/helper functions.registerHook
β Registers React hooks for dependency injection.registerComponent
β Registers React components with injected dependencies.
β‘ Initialization utils
initContainer
β An asynchronous function that initializes the container. It ensures that all dependencies are registered and ready to be used.ContainerInitializer
β A React component that wraps your app. It only renders its children after the container is fully initialized, ensuring a safe and stable dependency environment.
π‘ Usage Examples
Letβs walk through a practical example of how EyeOSee can simplify dependency management and testing in your React application. Imagine we want to build a User Profile feature that fetches user data from an API and displays it.
ποΈ Project Structure
To follow the Single Responsibility Principle, we'll split the logic into:
config
β An object containing the configuration.fetchUser
β A function to handle the API call.useUser
β A custom hook to manage data fetching and loading states.UserProfile
β A React component that displays the user data.
Without EyeOSee, these entities would tightly import each other, making unit testing cumbersome. With EyeOSee, we inject dependencies, making everything more modular and easier to test.
1οΈβ£ Setting the config
object
import { registerConfig } from "./eyeosee-container.gen";
// β
Register the CONFIG object in the container
// This makes the object available for dependency injection.
export const config = registerConfig("CONFIG", {
API_BASE_URL: "https://my-api.com"
})
π Explanation:
- Dependency Injection::
registerConfig
makesCONFIG
object available for injection
2οΈβ£ Creating the fetchUser
Function
import { registerFunction } from "./eyeosee-container.gen";
// β
Define the User type to ensure type safety across the app
// This prevents errors when dealing with user data.
type User = {
id: number;
name: string;
avatarUrl: string;
};
// β
Register the fetchUser function in the container
// This makes the function available for dependency injection.
export const fetchUser = registerFunction(
"fetchUser",
["CONFIG"] // π Inject CONFIG in the dependencies
)<
[id: number], // π Arguments: a `number` argument for user ID
Promise<User> // π Return type: a Promise resolving to a `User` object
>(async (id, deps) => {
// π Perform the API call to fetch user data
return await fetch(`${deps.CONFIG.API_BASE_URL}/users/${id}`).then((r) => r.json());
});
π Explanation:
- Typed Arguments:
[id: number]
defines that the function expects a number as input. - Typed Return:
Promise<User>
enforces that the function returns aUser
object wrapped in a promise. - Dependency Injection:
fetchUser
recieves theCONFIG
object in thedeps
argument.
3οΈβ£ Creating the useUser
Hook
import { useState, useEffect } from "react";
import { registerHook } from "./eyeosee-container.gen";
import type { User } from "./fetchUser";
// β
Define the state type to manage user data and loading state
export type UserState = {
user?: User;
isLoading: boolean;
};
// β
Register the useUser hook with fetchUser as a dependency
export const useUser = registerHook("useUser", ["fetchUser"])<
[id: number], // π Arguments: a `number` argument for user ID
UserState // Return type: a `UserState` object
>((id, deps) => {
const [state, setState] = useState<UserState>({
user: undefined,
isLoading: true,
});
useEffect(() => {
setState({ user: undefined, isLoading: true });
// π£οΈ Calls "fetchUser" from the "deps" paramater automatically
// appended in the hook's arguments
deps.fetchUser(id).then((user) => {
setState({ user, isLoading: false });
});
}, [id]);
return state;
});
π Explanation:
- Typed Arguments:
[id: number]
is the list of arguments expected by the hook. - Typed Return:
UserState
is the return type of the hook. - Dependency Injection:
fetchUser
is injected usingdeps
to avoid direct imports.
4οΈβ£ Creating the UserProfile
Component
import { registerComponent } from './eyeosee-container.gen';
// β
Define props for the UserProfile component
// This ensures that only the correct props can be passed to the component.
type UserProfileProps = {
id: number;
};
// β
Register the UserProfile component and inject the useUser hook
export const UserProfile = registerComponent("UserProfile", ["useUser"])
<UserProfileProps>(({ id, __deps }) => {
// π Injected useUser hook is accessed via __deps that is
// automatically added to component's props
const { user, isLoading } = __deps.useUser(id);
if (isLoading) {
return <span>Loading...</span>; // β³ Show loading state
}
// β
Display user data when loaded
return (
<div>
<img data-testid="avatar" src={user.avatarUrl} alt={user.name} />
<span data-testid="name">{user.name}</span>
</div>
);
});
π Explanation:
- Typed Props:
<UserProfileProps>
strictly defines props expected by the component. - Dependency Injection:
useUser
is injected using__deps
to avoid direct imports.
4οΈβ£ Rendering the Component
import React from 'react';
import ReactDOM from 'react-dom';
import { ContainerInitializer } from './eyeosee-container.gen';
import { UserProfile } from './UserProfile';
const rootElement = document.getElementById("root")!;
// π Initialize the app with the dependency container
ReactDOM.createRoot(rootElement).render(
<React.StrictMode>
{/* π Ensure the container is ready before rendering components */}
<ContainerInitializer fallback={<p>Loading container...</p>}>
<UserProfile id={42} />
</ContainerInitializer>
</React.StrictMode>
);
π Explanation:
ContainerInitializer
: Ensures dependencies are ready before rendering.- Fallback UI: Displays a loading message while initializing the container.
π§ͺ Testing
Now that we have set up dependency injection with EyeOSee, let's explore how it simplifies testing in our React applications. By isolating dependencies, EyeOSee makes unit tests more reliable and easier to write.
1οΈβ£ Testing the fetchUser
Function
As the fetchUser
doesn't have any dependencies, we can test it as we would test any regular function.
In our case, as this function performs network calls, we can simply mock them using a library like nock
.
import { beforeAll, test, expect } from "vitest";
import nock from "nock";
import { initContainer } from "./eyeosee-container.gen";
import { fetchUser } from "./fetchUser";
// β
Initialize the container before running tests
beforeAll(initContainer);
test("fetchUser fetches the user data", async () => {
const TEST_API_URL = "https://my-api.test";
// π Mock the API call
const scope = nock(TEST_API_URL).get("/users/42").reply(200, {
id: 42,
name: "John Smith",
avatarUrl: "https://cdn.acme.com/users/42.png",
});
// π₯ Call the fetchUser function
const user = await fetchUser(42, {
// Override the value of the CONFIG object to provide
// our own value
CONFIG: { API_BASE_URL: TEST_API_URL }
});
// β
Ensure the mock API was called
scope.done();
// π§ Validate the result
expect(user).toEqual({
id: 42,
name: "John Smith",
avatarUrl: "https://cdn.acme.com/users/42.png",
});
});
π Explanation:
beforeAll(initContainer)
ensures the dependency container is initialized before tests run.nock
mocks the external API, eliminating real network calls.- The test verifies that
fetchUser
properly retrieves and formats user data. - The value of a dependency can be overriden when calling the function
2οΈβ£ Testing the useUser
Hook in Isolation
Hooks often have dependencies that make them tricky to test. With EyeOSee, you can override dependencies to isolate the hook logic.
import { beforeAll, test, expect } from "vitest";
import { renderHook, act, waitFor } from "@testing-library/react";
import { initContainer } from "./eyeosee-container.gen";
import { useUser } from "./useUser";
import type { User } from "./fetchUser";
// β
Initialize the container
beforeAll(initContainer);
test("useUser loads user data", async () => {
// π§ Create a mock promise
const { promise, resolve } = Promise.withResolvers<User>();
// π§ͺ Render the hook with a mock dependency
const { result } = renderHook(() =>
useUser(42, {
fetchUser: (id) => {
expect(id).toEqual(42);
return promise;
},
})
);
// β³ Assert initial loading state
expect(result.current).toEqual({ isLoading: true, user: undefined });
// π Simulate the promise resolving
act(() => {
resolve({
id: 42,
name: "John Smith",
avatarUrl: "https://cdn.acme.com/users/42.png",
});
});
await waitFor(() => {
// β
Check if the hook updated correctly
expect(result.current).toEqual({
isLoading: false,
user: {
id: 42,
name: "John Smith",
avatarUrl: "https://cdn.acme.com/users/42.png",
},
});
});
});
π Explanation:
- The hook is rendered with a mocked
fetchUser
function. - By manually resolving the promise, we simulate async data fetching.
- This isolates and tests the hook without real API calls.
3οΈβ£ Testing the UserProfile
Component
Components often combine multiple pieces of logic, but with EyeOSee, we can easily mock dependencies to isolate the UI logic.
import { beforeAll, test, expect } from "vitest";
import { render, screen } from "@testing-library/react";
import { initContainer } from "./eyeosee-container.gen";
import { UserProfile } from "./UserProfile";
// β
Initialize the container
beforeAll(initContainer);
test("UserProfile displays a loading state", () => {
// π§ͺ Render with mocked loading state
render(
<UserProfile
id={42}
__deps={{
useUser: (id) => {
// π§ Verify that the right id is passed
expect(id).toEqual(42);
return { isLoading: true, user: undefined };
},
}}
/>
);
// π Check for loading indicator
expect(screen.getByText("Loading...")).toBeInTheDocument();
});
test("UserProfile displays user data", () => {
// π§ͺ Render with mock user data
render(
<UserProfile
id={42}
__deps={{
useUser: () => ({
isLoading: false,
user: {
id: 42,
name: "John Smith",
avatarUrl: "https://cdn.acme.com/users/42.png",
},
}),
}}
/>
);
// π§ Verify the displayed content
expect(screen.getByTestId("name")).toHaveTextContent("John Smith");
expect(screen.getByTestId("avatar")).toHaveAttribute(
"src",
"https://cdn.acme.com/users/42.png"
);
});
π Explanation:
- We mock the
useUser
hook to control the component's state. - The component is tested in both the loading and loaded states.
By injecting mock implementations of dependencies, EyeOSee simplifies component testing. No need to worry about internal state transitionsβjust focus on the component behavior!
π Conclusion
EyeOSee empowers React developers to build scalable, maintainable, and testable applications by introducing a seamless dependency injection system. Hereβs why EyeOSee stands out:
π― Key Advantages
- Simplified Dependency Management: Automatically manage and inject dependencies without cluttered imports.
- Enhanced Type Safety: Define clear input and output types for functions, hooks, and components, reducing runtime errors.
- Streamlined Testing: Effortlessly mock dependencies, enabling isolated and reliable unit tests.
- Improved Code Organization: Promote clean architecture by separating concerns between helpers, hooks, and components.
- Scalability: Easily adapt and extend applications without introducing tight coupling or complex refactoring.
By leveraging EyeOSee, you ensure that your React projects are more modular, flexible, and robust. Whether youβre working on small features or large-scale applications, EyeOSee makes dependency management clear and hassle-free.
So, why wait? Start building cleaner, more maintainable React apps today with EyeOSee! ποΈβ¨
π Contributing
Contributions are welcome! Feel free to open issues or submit pull requests.
π License
MIT License. Use it freely and responsibly.
Happy Coding! π