0.0.70 • Published 1 year ago

@eleven-am/notifier v0.0.70

Weekly downloads
-
License
MIT
Repository
-
Last release
1 year ago

Notifier

A powerful and type-safe state management solution for React applications, featuring class-based state management, selectors, and event handling.

npm version License

Features

  • 🎯 Type-safe: Full TypeScript support with precise type inference
  • 🏗️ Class-based Architecture: Clean and organized state management
  • 🔄 Selectors: Compute derived state with memoization
  • 📡 Event System: Built-in pub/sub pattern for component communication
  • 🎣 Custom Hooks: Generate specialized hooks for state, actions, and events
  • 🏭 Factory Pattern: Create local state instances from global notifiers
  • 🔒 Encapsulation: Protected state access with controlled updates

Installation

npm install @eleven-am/notifier

Basic Usage

1. Create a Notifier

import { Notifier } from '@eleven-am/notifier';

interface UserState {
  name: string;
  age: number;
}

class UserNotifier extends Notifier<UserState> {
  setName(name: string) {
    this.updateState({ name });
  }

  setAge(age: number) {
    this.updateState({ age });
  }
}

// Initialize with default state
const userNotifier = new UserNotifier({
  name: 'John Doe',
  age: 25
});

2. Create and Use Hooks

// Create hooks for global state management
const useUser = userNotifier.createStateHook();
const useUserActions = userNotifier.createActionsHook();

// Use in components
function UserProfile() {
  // Optional transform function
  const { name, age } = useUser(state => ({
    name: state.name.toUpperCase(),
    age: state.age
  }));
  
  const { setName, setAge } = useUserActions();

  return (
    <div>
      <input 
        value={name}
        onChange={e => setName(e.target.value)}
      />
      <input 
        type="number"
        value={age}
        onChange={e => setAge(Number(e.target.value))}
      />
    </div>
  );
}

Advanced Features

Factory Pattern

The factory pattern allows you to create local instances of a notifier, enabling component-specific state management instead of global state. This is particularly useful when you need multiple independent instances of the same state structure:

// Create a factory hook with initial state
const userFactory = UserNotifier.createFactoryHook({
  name: 'John Doe',
  age: 25
});

// Use in components for local state management
function UserCard() {
  // Each component gets its own instance of the state
  const { name, age } = userFactory();
  
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
    </div>
  );
}

// Multiple instances with independent state
function UserList() {
  return (
    <div>
      <UserCard /> {/* Has its own state */}
      <UserCard /> {/* Has different independent state */}
      <UserCard /> {/* Has different independent state */}
    </div>
  );
}

The key differences between global and local state management:

// Global State (shared across all components)
const globalUserNotifier = new UserNotifier({ name: 'John', age: 25 });
const useGlobalUser = globalUserNotifier.createStateHook();

// Local State (independent for each component)
const useLocalUser = UserNotifier.createFactoryHook({ name: 'John', age: 25 });

function App() {
  // These components share the same state
  return (
    <>
      <GlobalUserComponent />
      <GlobalUserComponent /> {/* Updates reflect in both components */}
    </>
  );
}

function LocalStateApp() {
  // These components have independent states
  return (
    <>
      <LocalUserComponent /> {/* Independent state */}
      <LocalUserComponent /> {/* Different independent state */}
    </>
  );
}

Selectors

Create computed state derived from one or more notifiers:

import { selector } from '@eleven-am/notifier';

const userDetailsSelector = selector((get, set) => {
  const user = get(userNotifier);
  const preferences = get(preferencesNotifier);
  
  return {
    fullName: `${user.name} (${preferences.nickname})`,
    isAdult: user.age >= 18
  };
});

// Create hook for the selector
const useUserDetails = userDetailsSelector.createStateHook();

Event Notifier

Handle pub/sub events between components:

import { EventNotifier } from '@eleven-am/notifier';

interface ChatState {
  messages: string[];
}

interface ChatEvents {
  messageReceived: string;
  typing: { userId: string };
}

class ChatNotifier extends EventNotifier<ChatState, ChatEvents> {
  addMessage(message: string) {
    this.updateState({
      messages: [...this.state.messages, message]
    });
    this.emit('messageReceived', message);
  }

  setTyping(userId: string) {
    this.emit('typing', { userId });
  }
}

// In components
function ChatRoom() {
  const { on } = useChatActions();
  
  // Method 1: Using the events hook
  useChatEvents('messageReceived', (message) => {
    console.log('New message:', message);
  });
  
  // Method 2: Using direct subscription
  useEffect(() => {
    const unsubscribe = on('typing', ({ userId }) => {
      console.log(`${userId} is typing...`);
    });
    
    return unsubscribe;
  }, [on]);
}

Type Definitions

Key types for advanced usage:

// Selector function type
type SelectorFunc<State, ReturnType> = (state: State) => ReturnType;

// Hook for accessing notifier state
type UseNotifierHook<State> = <ReturnType = State>(
  selector?: SelectorFunc<State, ReturnType>
) => ReturnType;

// Hook for accessing notifier methods
type UseActorsHook<Class extends Notifier<any>> = () => 
  PublicMethods<Class>;

// Event subscription callback
type Observer<Data> = (data: Data) => void;

// Event hook type
type UseEventHook<EventType> = <Event extends keyof EventType>(
  event: Event, 
  callback: (data: EventType[Event]) => void
) => void;

Why Class-Based State Management?

The Notifier package leverages classes for state management, providing several key advantages over traditional object-based approaches:

1. Inheritance and Extension

// Base authentication notifier with common functionality
class AuthNotifier extends Notifier<AuthState> {
  login(credentials: Credentials) {
    // Common login logic
  }
  
  logout() {
    // Common logout logic
  }
}

// Specialized authentication for different providers
class OAuth2Notifier extends AuthNotifier {
  login(credentials: OAuth2Credentials) {
    // OAuth2 specific logic
    super.login(credentials);
    this.handleTokenRefresh();
  }
  
  private handleTokenRefresh() {
    // Token refresh logic
  }
}

class BasicAuthNotifier extends AuthNotifier {
  login(credentials: BasicAuthCredentials) {
    // Basic auth specific logic
    super.login(credentials);
  }
}

2. Encapsulation and Privacy

class UserNotifier extends Notifier<UserState> {
  private validateAge(age: number) {
    if (age < 0 || age > 150) {
      throw new Error('Invalid age');
    }
  }

  setAge(age: number) {
    this.validateAge(age);
    this.updateState({ age });
  }
  
  // State can only be modified through defined methods
  // No direct external state manipulation possible
}

3. Method Organization and Code Structure

class ShoppingCartNotifier extends Notifier<CartState> {
  // Clear grouping of related functionality
  // Cart Items Management
  addItem(item: Product) { /* ... */ }
  removeItem(itemId: string) { /* ... */ }
  updateQuantity(itemId: string, quantity: number) { /* ... */ }
  
  // Cart Totals
  calculateSubtotal() { /* ... */ }
  calculateTax() { /* ... */ }
  calculateTotal() { /* ... */ }
  
  // Checkout Process
  beginCheckout() { /* ... */ }
  applyDiscount(code: string) { /* ... */ }
  completeTransaction() { /* ... */ }
}

4. Type Safety and Intellisense

class ProductNotifier extends Notifier<ProductState> {
  // Methods and properties are properly typed
  // IDE provides excellent autocomplete and type checking
  updateStock(productId: string, quantity: number) {
    const product = this.state.products[productId];
    if (!product) throw new Error('Product not found');
    
    this.updateState({
      products: {
        ...this.state.products,
        [productId]: {
          ...product,
          stock: quantity
        }
      }
    });
  }
}

5. Testing and Mocking

class ApiNotifier extends Notifier<ApiState> {
  protected api: ApiService;
  
  constructor(initialState: ApiState, api: ApiService) {
    super(initialState);
    this.api = api;
  }
  
  async fetchData() {
    const data = await this.api.getData();
    this.updateState({ data });
  }
}

// Easy to test with mock services
describe('ApiNotifier', () => {
  it('should fetch and update data', async () => {
    const mockApi = new MockApiService();
    const notifier = new ApiNotifier(initialState, mockApi);
    await notifier.fetchData();
    expect(notifier.state.data).toEqual(expectedData);
  });
});

6. Cross-Cutting Concerns

class LoggingNotifier<T> extends Notifier<T> {
  protected updateState(state: Partial<T>) {
    console.log('State update:', state);
    super.updateState(state);
    console.log('New state:', this.state);
  }
}

// Easily add logging to any notifier
class UserNotifier extends LoggingNotifier<UserState> {
  // All state updates are automatically logged
  updateProfile(profile: Partial<UserState>) {
    this.updateState(profile);
  }
}

Best Practices

  1. Global vs Local State: Use regular notifier instances for global state and factory hooks for component-specific state
  2. State Updates: Use updateState method for partial updates instead of directly setting state
  3. Event Cleanup: Always unsubscribe from events in useEffect cleanup function
  4. Selector Memoization: Use selectors for computed values that depend on multiple state sources
  5. Type Safety: Leverage TypeScript interfaces for state and event types
  6. Encapsulation: Keep state modifications within the notifier class methods

Contributing

We welcome contributions! Please see our contributing guidelines for details.

License

MIT © Roy OSSAI

0.0.70

1 year ago

0.0.69

1 year ago

0.0.68

2 years ago

0.0.62

2 years ago

0.0.63

2 years ago

0.0.64

2 years ago

0.0.65

2 years ago

0.0.66

2 years ago

0.0.67

2 years ago

0.0.60

2 years ago

0.0.61

2 years ago

0.0.59

2 years ago

0.0.58

2 years ago

0.0.57

2 years ago

0.0.52

2 years ago

0.0.53

2 years ago

0.0.54

2 years ago

0.0.55

2 years ago

0.0.56

2 years ago

0.0.51

2 years ago

0.0.50

2 years ago

0.0.48

2 years ago

0.0.49

2 years ago

0.0.47

2 years ago

0.0.40

2 years ago

0.0.41

2 years ago

0.0.45

2 years ago

0.0.46

2 years ago

0.0.37

2 years ago

0.0.38

2 years ago

0.0.39

2 years ago

0.0.32

2 years ago

0.0.33

2 years ago

0.0.34

2 years ago

0.0.35

2 years ago

0.0.36

2 years ago

0.0.31

3 years ago

0.0.30

3 years ago

0.0.29

3 years ago

0.0.28

3 years ago

0.0.27

3 years ago

0.0.26

3 years ago

0.0.25

3 years ago

0.0.24

3 years ago

0.0.23

3 years ago

0.0.22

3 years ago

0.0.21

3 years ago

0.0.20

3 years ago

0.0.19

3 years ago

0.0.18

3 years ago

0.0.17

3 years ago

0.0.16

3 years ago

0.0.15

3 years ago

0.0.14

3 years ago

0.0.13

3 years ago

0.0.12

3 years ago

0.0.11

3 years ago

0.0.10

3 years ago

0.0.9

3 years ago

0.0.8

3 years ago

0.0.7

3 years ago

0.0.6

3 years ago

0.0.5

3 years ago

0.0.4

3 years ago

0.0.3

3 years ago