npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

rtk-service-loader

v1.0.2

Published

Dynamic service injection for Redux Toolkit with Redux Saga support

Readme

🚀 RTK Service Loader

Dynamic service injection for Redux Toolkit with Redux Saga support. Built for modern React applications.

npm version TypeScript License: MIT

✨ Features

  • 🎯 Dynamic Injection: Load reducers and sagas on-demand
  • 🔄 Reference Counting: Automatic cleanup when services are no longer needed
  • 📘 TypeScript First: Full type safety and IntelliSense
  • ⚛️ React 18+: Compatible with modern React including StrictMode
  • 🎨 Redux Toolkit: Built for RTK patterns
  • 🔥 Redux Saga: First-class saga support
  • 🧹 Memory Safe: Prevents memory leaks with automatic cleanup
  • 🎣 Lifecycle Hooks: Before/after initialization callbacks
  • 💾 Keep Alive: Optional persistent services
  • 🔌 HOC Pattern: withServices wrapper

📦 Installation

npm install rtk-service-loader

Peer Dependencies

npm install @reduxjs/toolkit react react-redux redux redux-saga

🚀 Quick Start

1. Setup Store

Create your Redux store with the reducer manager:

// store/index.ts
import { configureStore } from '@reduxjs/toolkit';
import createSagaMiddleware from 'redux-saga';
import { createReducerManager, createService } from 'rtk-service-loader';

// Create saga middleware
export const sagaMiddleware = createSagaMiddleware();

// Create reducer manager
export const reducerManager = createReducerManager({});

// Configure store
export const store = configureStore({
  reducer: reducerManager.reduce,
  middleware: (getDefaultMiddleware) =>
    getDefaultMiddleware({ thunk: false }).concat(sagaMiddleware),
});

// Create service creator
export const serviceCreator = createService(
  store,
  reducerManager,
  sagaMiddleware
);

// Export types
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;

2. Create a Service

Create a feature service with reducer and saga:

// features/counter/counterService.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { put, takeLatest, delay } from 'redux-saga/effects';
import { serviceCreator } from '../../store';

export const COUNTER_SERVICE_NAME = 'counter';

// State interface
interface CounterState {
  value: number;
  loading: boolean;
}

// Slice
const counterSlice = createSlice({
  name: COUNTER_SERVICE_NAME,
  initialState: {
    value: 0,
    loading: false,
  } as CounterState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
    setLoading: (state, action: PayloadAction<boolean>) => {
      state.loading = action.payload;
    },
  },
});

export const { increment, decrement, setLoading } = counterSlice.actions;

// Saga
function* incrementAsync(): Generator<any, void, any> {
  yield put(setLoading(true));
  yield delay(1000);
  yield put(increment());
  yield put(setLoading(false));
}

function* counterSaga() {
  yield takeLatest('counter/incrementAsync', incrementAsync);
}

// Create service hook
export const useCounterService = serviceCreator({
  id: COUNTER_SERVICE_NAME,
  reducersMap: {
    [COUNTER_SERVICE_NAME]: counterSlice.reducer,
  },
  sagas: [counterSaga],
  before: (store) => {
    console.log('🔢 Counter service initializing...');
  },
  after: () => {
    console.log('✅ Counter service ready!');
  },
});

// Export service loader for HOC pattern
export const importCounterService = () => useCounterService;

// Action creators
export const incrementAsync = () => ({ type: 'counter/incrementAsync' });

🎨 Usage Patterns

Pattern 1: Direct Hook Usage (Simple)

Use the service hook directly in your component:

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useCounterService, increment, incrementAsync } from './counterService';
import type { RootState } from '../../store';

function CounterPage() {
  // Inject the service
  const { isActive } = useCounterService();

  const { value, loading } = useSelector(
    (state: RootState) => state.counter ?? { value: 0, loading: false }
  );
  const dispatch = useDispatch();

  if (!isActive) return <div>Loading service...</div>;

  return (
    <div>
      <h1>Counter: {value}</h1>
      <button onClick={() => dispatch(increment())}>+1</button>
      <button onClick={() => dispatch(incrementAsync())} disabled={loading}>
        +1 Async {loading && '(loading...)'}
      </button>
    </div>
  );
}

Pattern 2: HOC Wrapper

Wrap your pages with services using withServices HOC:

import { withServices } from 'rtk-service-loader';
import { importCounterService } from './features/counter/counterService';
import { importUserService } from './features/user/userService';

// Single service wrapper
export const CounterService: React.FC<{ children: React.ReactNode }> =
  withServices([importCounterService])(({ children }) => <>{children}</>);

// Multiple services wrapper
export const AppServices: React.FC<{ children: React.ReactNode }> =
  withServices([
    importCounterService,
    importUserService,
  ])(({ children }) => <>{children}</>);

// Usage in pages
function CounterPage() {
  return (
    <CounterService>
      <CounterContent />
    </CounterService>
  );
}

// Or wrap the entire page
const CounterPageWithService = withServices([importCounterService])(CounterPage);

Pattern 3: ServiceProvider Component

Use the declarative ServiceProvider component:

import { ServiceProvider } from 'rtk-service-loader';
import { useCounterService } from './features/counter/counterService';
import { useAuthService } from './features/auth/authService';

function DashboardPage() {
  return (
    <ServiceProvider services={[useAuthService, useCounterService]}>
      <DashboardContent />
    </ServiceProvider>
  );
}

Pattern 4: Route-Based Service Loading

Load services per route for code splitting:

// routes.tsx
import { lazy } from 'react';
import { withServices } from 'rtk-service-loader';
import { importCounterService } from './features/counter/counterService';
import { importFormsService } from './features/forms/formsService';

// Lazy load page components
const CounterPage = lazy(() => import('./pages/CounterPage'));
const FormsPage = lazy(() => import('./pages/FormsPage'));

// Wrap routes with their services
const CounterRoute = withServices([importCounterService])(CounterPage);
const FormsRoute = withServices([importFormsService])(FormsPage);

// Router configuration
export const routes = [
  {
    path: '/counter',
    element: <CounterRoute />,
  },
  {
    path: '/forms',
    element: <FormsRoute />,
  },
];

Pattern 5: Layout-Level Services

Load common services at layout level:

import { Outlet } from 'react-router-dom';
import { withServices } from 'rtk-service-loader';
import { importAuthService } from './features/auth/authService';
import { importNotificationService } from './features/notifications/notificationService';

// Base layout component
const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
  <div className="app-layout">
    <Header />
    <Sidebar />
    <main>{children}</main>
    <Footer />
  </div>
);

// Wrap layout with global services
export const AuthenticatedLayout = withServices([
  importAuthService,
  importNotificationService,
])(AppLayout);

// Router setup
<Routes>
  <Route element={<AuthenticatedLayout />}>
    <Route path="/dashboard" element={<Dashboard />} />
    <Route path="/profile" element={<Profile />} />
    <Route path="/settings" element={<Settings />} />
  </Route>
</Routes>

Pattern 6: Nested Services (Feature Modules)

Organize services by feature modules:

// features/forms/index.tsx
import { withServices } from 'rtk-service-loader';
import { importFormsService } from './formsService';
import { importFormsValidationService } from './validationService';

// Forms module wrapper
export const FormsModule = withServices([
  importFormsService,
  importFormsValidationService,
])(({ children }: { children: React.ReactNode }) => <>{children}</>);

// Usage
function FormsSection() {
  return (
    <FormsModule>
      <FormsList />
      <FormEditor />
      <FormPreview />
    </FormsModule>
  );
}

Pattern 7: Conditional Service Loading

Load services based on conditions:

import { ServiceProvider } from 'rtk-service-loader';
import { useAuthService } from './features/auth/authService';
import { useAdminService } from './features/admin/adminService';

function AdminDashboard({ isAdmin }: { isAdmin: boolean }) {
  // Conditionally include admin service
  const services = [
    useAuthService,
    ...(isAdmin ? [useAdminService] : []),
  ];

  return (
    <ServiceProvider services={services}>
      <DashboardContent />
    </ServiceProvider>
  );
}

🏗️ Architecture Examples

Example 1: Feature-Based Architecture

src/
├── store/
│   └── index.ts                    # Store setup
├── features/
│   ├── auth/
│   │   ├── authService.ts         # Service definition
│   │   ├── AuthProvider.tsx       # Service wrapper
│   │   └── components/
│   │       ├── LoginForm.tsx
│   │       └── UserMenu.tsx
│   ├── forms/
│   │   ├── formsService.ts
│   │   ├── FormsProvider.tsx
│   │   └── components/
│   └── notifications/
│       ├── notificationsService.ts
│       └── NotificationsProvider.tsx
└── pages/
    ├── DashboardPage.tsx
    └── FormsPage.tsx

Example 2: Domain-Driven Architecture

// domains/user/userService.ts
export const useUserService = serviceCreator({
  id: 'user',
  reducersMap: { user: userReducer },
  sagas: [userSaga],
});

// domains/user/UserDomain.tsx
export const UserDomain = withServices([() => useUserService])(
  ({ children }) => children
);

// app/App.tsx
<UserDomain>
  <Router>
    <Routes>
      <Route path="/profile" element={<ProfilePage />} />
      <Route path="/settings" element={<SettingsPage />} />
    </Routes>
  </Router>
</UserDomain>

Example 3: Micro-Frontend Architecture

// Lazy load feature modules with their services
const FormsApp = lazy(() => import('./apps/forms/FormsApp'));
const AdminApp = lazy(() => import('./apps/admin/AdminApp'));

// Each app brings its own services
function App() {
  return (
    <Suspense fallback={<Loading />}>
      <Routes>
        <Route path="/forms/*" element={<FormsApp />} />
        <Route path="/admin/*" element={<AdminApp />} />
      </Routes>
    </Suspense>
  );
}

// apps/forms/FormsApp.tsx
export default withServices([
  importFormsService,
  importFormsValidationService,
])(FormsApplication);

📖 API Reference

createReducerManager(initialReducers?)

Creates a reducer manager for dynamic injection.

Parameters:

  • initialReducers (optional): Initial reducer map

Returns: ReducerManager

createService(store, reducerManager, sagaMiddleware)

Creates a service creator function.

Parameters:

  • store: Redux store instance
  • reducerManager: Reducer manager instance
  • sagaMiddleware: Redux Saga middleware instance

Returns: ServiceCreator

serviceCreator(config)

Creates a service hook.

Config:

interface ServiceConfig {
  id: string;                              // Unique service identifier (required)
  reducersMap?: Record<string, Reducer>;   // Reducers to inject
  sagas?: Array<() => Generator>;          // Saga generator functions
  before?: (store: Store) => void;         // Pre-initialization hook
  after?: (store: Store) => void;          // Post-initialization hook
  keepAlive?: boolean;                     // Prevent cleanup (default: false)
  logger?: (...args: any[]) => void;       // Custom logger
}

Returns: Hook function () => UseServiceReturn

withServices(serviceLoaders)

Higher-order component for service injection.

Parameters:

  • serviceLoaders: Array of service loader functions

Returns: HOC function

ServiceProvider

Component for declarative service injection.

Props:

interface ServiceProviderProps {
  services: Array<() => UseServiceReturn>;
  children: React.ReactNode;
}

Utility Functions

getActiveServices(): string[] Returns array of active service IDs.

isServiceActive(id: string): boolean Check if a service is currently active.

cleanupServiceById(id, store, reducerManager, logger?) Manually cleanup a specific service.

getActiveServicesMap(): Map<string, ServiceData> Returns the internal services map (for debugging).

🔥 Advanced Features

Lifecycle Hooks

const useMyService = serviceCreator({
  id: 'myService',
  reducersMap: { myFeature: myReducer },
  before: (store) => {
    console.log('Service initializing...', store.getState());
    // Initialize analytics, load cached data, etc.
  },
  after: (store) => {
    console.log('Service ready!', store.getState());
    // Trigger initial data fetch
    store.dispatch({ type: 'myFeature/init' });
  },
});

Keep Alive Services

// Global services that should never cleanup
const useAuthService = serviceCreator({
  id: 'auth',
  reducersMap: { auth: authReducer },
  sagas: [authSaga],
  keepAlive: true, // Service persists across navigation
});

Custom Logger

import { createLogger } from './logger';

const useMyService = serviceCreator({
  id: 'myService',
  reducersMap: { feature: featureReducer },
  logger: createLogger('MyService'),
});

Reference Counting

Services automatically track usage across multiple components:

function ComponentA() {
  useMyService(); // ref count: 1
  return <div>A</div>;
}

function ComponentB() {
  useMyService(); // ref count: 2 (reuses existing service)
  return <div>B</div>;
}

// When ComponentA unmounts: ref count: 1
// When ComponentB unmounts: ref count: 0 → cleanup triggered

🧪 Testing

Services are fully testable:

import { renderHook, act } from '@testing-library/react';
import { Provider } from 'react-redux';
import { useCounterService } from './counterService';

test('counter service loads correctly', async () => {
  const { result } = renderHook(() => useCounterService(), {
    wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
  });

  await act(async () => {
    // Wait for service to initialize
  });

  expect(result.current.isActive).toBe(true);
  expect(store.getState()).toHaveProperty('counter');
});

TypeScript Support

Full TypeScript support with proper type inference:

interface RootState {
  counter?: { value: number; loading: boolean };
}

const useCounterService = serviceCreator({
  id: 'counter',
  reducersMap: { counter: counterReducer },
});

// Type-safe selector
const counter = useSelector((state: RootState) => state.counter);

💡 Best Practices

  1. Unique Service IDs: Always use unique, descriptive service identifiers
  2. Lazy Loading: Combine with React.lazy() for code splitting
  3. Memory Management: Avoid keepAlive unless the service is truly global
  4. Error Boundaries: Wrap service-loaded components in error boundaries
  5. Type Safety: Define proper TypeScript interfaces for your state
  6. Testing: Test services in isolation before integration
  7. Documentation: Document which services each page/feature requires

🤔 When to Use

✅ Good Use Cases:

  • Large applications with many features
  • Code splitting and lazy loading
  • Feature-based architecture
  • Micro-frontends
  • Dynamic module loading
  • Route-based service loading

❌ Not Recommended:

  • Small applications with simple state
  • When all state is needed globally
  • Static, unchanging state structure

📝 License

MIT © [Your Name]

🤝 Contributing

Contributions welcome! Please read our contributing guide.

🐛 Issues

Found a bug? Create an issue

🔗 Links