Hone logo
Hone
Problems

Minimalist State Management Library for React

Building a state management library is a fundamental exercise in understanding React's internals and architectural patterns. This challenge asks you to create a simplified state management library, similar in concept to Redux, but with a reduced feature set to focus on core principles. This will help solidify your understanding of state immutability, reducers, and dispatching actions.

Problem Description

You are tasked with building a basic state management library for React, named MiniState. The library should provide the following functionalities:

  1. createStore(reducer: Reducer<State>): This function takes a reducer function as an argument and returns a store object. The reducer is a function that takes the current state and an action and returns a new state.
  2. Store object: The store object should have the following methods:
    • getState(): State: Returns the current state of the store.
    • subscribe(listener: (state: State) => void): UnsubscribeFunction: Registers a listener function that will be called whenever the state changes. The function should return an UnsubscribeFunction which, when called, removes the listener.
    • dispatch(action: Action): void: Dispatches an action to the store. This should trigger the reducer function, update the state, and notify all subscribed listeners.
  3. Reducer<State>: A type representing a reducer function. It takes the current state and an action and returns a new state.
  4. State: A type representing the application's state. This is intentionally generic to allow for any state structure.
  5. Action: A type representing an action. This is intentionally generic to allow for any action structure.
  6. UnsubscribeFunction: A type representing a function that unsubscribes a listener.

The library should ensure that state updates are immutable (i.e., the reducer should return a new state object, not modify the existing one).

Expected Behavior:

  • The createStore function should initialize a store with an initial state of undefined.
  • getState should return the current state.
  • subscribe should call the listener function immediately with the initial state and then again whenever the state changes.
  • dispatch should call the reducer function with the current state and the action, update the store's state, and call all subscribed listeners with the new state.
  • unsubscribe should remove the listener from the list of subscribers.

Edge Cases to Consider:

  • Multiple subscriptions to the same store.
  • Dispatching actions after all listeners have unsubscribed. (Should not throw an error, but also should not do anything).
  • Reducer returning undefined or null. (Should be handled gracefully, ideally by returning the previous state).
  • Invalid reducer function (e.g., doesn't return a state). (Should be handled gracefully, ideally by returning the previous state).

Examples

Example 1:

// Assume MiniState is imported
import { createStore } from './mini-state'; // Replace with your actual path

interface State {
  count: number;
}

type Action =
  | { type: 'INCREMENT' }
  | { type: 'DECREMENT' };

const reducer = (state: State | undefined, action: Action): State => {
  if (state === undefined) {
    return { count: 0 };
  }

  switch (action.type) {
    case 'INCREMENT':
      return { ...state, count: state.count + 1 };
    case 'DECREMENT':
      return { ...state, count: state.count - 1 };
    default:
      return state;
  }
};

const store = createStore(reducer);

let listenerCalled = false;
const listener = (state: State) => {
  listenerCalled = true;
  console.log('State changed:', state);
};

store.subscribe(listener);

store.dispatch({ type: 'INCREMENT' });
console.log("State:", store.getState());
store.dispatch({ type: 'DECREMENT' });
console.log("State:", store.getState());

// Expected Output:
// State changed: { count: 1 }
// State: { count: 1 }
// State changed: { count: 0 }
// State: { count: 0 }

Example 2:

import { createStore } from './mini-state';

interface State {
  message: string;
}

type Action =
  | { type: 'SET_MESSAGE', payload: string }
  | { type: 'CLEAR_MESSAGE' };

const reducer = (state: State | undefined, action: Action): State => {
  if (state === undefined) {
    return { message: '' };
  }

  switch (action.type) {
    case 'SET_MESSAGE':
      return { ...state, message: action.payload };
    case 'CLEAR_MESSAGE':
      return { ...state, message: '' };
    default:
      return state;
  }
};

const store = createStore(reducer);

const unsubscribe = store.subscribe((state) => console.log(state.message));
store.dispatch({ type: 'SET_MESSAGE', payload: 'Hello, world!' });
store.dispatch({ type: 'CLEAR_MESSAGE' });
unsubscribe();
store.dispatch({ type: 'SET_MESSAGE', payload: 'Goodbye!' }); // Should not log anything

Constraints

  • The library should be implemented in TypeScript.
  • The state updates must be immutable.
  • The library should be relatively lightweight (avoid unnecessary dependencies).
  • The dispatch function should not throw an error even if no listeners are subscribed.
  • The reducer function should be able to handle an undefined initial state.
  • The library should not include any UI components or React hooks. It's purely a state management core.

Notes

  • Consider using closures to encapsulate the store's state and subscribers.
  • Think about how to efficiently manage the list of subscribers.
  • Focus on the core functionality and keep the implementation as simple as possible.
  • Error handling is not a primary focus, but graceful handling of unexpected inputs is appreciated.
  • This is a simplified version of a state management library. Real-world libraries often include features like middleware, time-travel debugging, and more sophisticated action handling. This challenge is designed to focus on the fundamental concepts.
Loading editor...
typescript