Hone logo
Hone
Problems

Minimalist State Management Library in JavaScript

This challenge asks you to build a simplified state management library in JavaScript, similar in concept to Redux but with a reduced feature set. Such a library is useful for managing application state predictably and efficiently, especially in larger applications where data flow can become complex. The goal is to create a core system for storing, updating, and subscribing to state changes.

Problem Description

You are tasked with creating a JavaScript library called MiniState. This library should provide the following core functionalities:

  1. createStore(reducer): This function takes a single argument: a reducer function. The reducer is a pure function that takes the current state and an action as input and returns the new state. The createStore function should return an object with the following methods:

    • getState(): Returns the current state.
    • dispatch(action): Dispatches an action to the store. This should call the reducer with the current state and the action, and update the internal state accordingly.
    • subscribe(listener): Takes a listener function as an argument. This function should be called whenever the state changes. The listener should receive the new state as an argument. The subscribe method should return an unsubscribe function. Calling the unsubscribe function should remove the listener from the list of subscribers.
  2. Actions: Actions are plain JavaScript objects that represent events that occur in the application. They are passed to the dispatch method. While the action object itself doesn't have a strict format, it's common practice to include a type property.

  3. Reducer: The reducer is a pure function that takes the current state and an action and returns the next state. It must be pure – it should not have any side effects and should always return the same output for the same input.

Expected Behavior:

  • The store should initialize with a default state of undefined.
  • getState() should return the current state.
  • dispatch() should call the reducer with the current state and the action, and update the internal state.
  • subscribe() should register a listener function.
  • Whenever dispatch() is called, all registered listeners should be called with the new state.
  • The unsubscribe function returned by subscribe() should correctly remove the listener.

Edge Cases to Consider:

  • What happens if the reducer throws an error? (For simplicity, you can choose to ignore errors or log them to the console.)
  • What happens if subscribe() is called multiple times with the same listener? (Consider whether you want to allow duplicate listeners.)
  • What happens if dispatch() is called with an action that the reducer doesn't handle? (The state should remain unchanged.)
  • What happens if the initial state is provided as an argument to createStore? (For this challenge, assume the initial state is always undefined.)

Examples

Example 1:

Input:
const reducer = (state = 0, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
};

const store = MiniState.createStore(reducer);

store.subscribe((state) => console.log('State:', state));

store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'INCREMENT' });
store.dispatch({ type: 'DECREMENT' });

Output:
State: 1
State: 2
State: 1

Explanation: The store is initialized with state 0. Each dispatch call updates the state and triggers the listener, which logs the new state to the console.

Example 2:

Input:
const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'ADD_COUNT':
      return { ...state, count: state.count + action.payload };
    default:
      return state;
  }
};

const store = MiniState.createStore(reducer);

const unsubscribe = store.subscribe((state) => {
  console.log('Count:', state.count);
});

store.dispatch({ type: 'ADD_COUNT', payload: 5 });
store.dispatch({ type: 'ADD_COUNT', payload: 2 });

unsubscribe(); // Remove the listener

store.dispatch({ type: 'ADD_COUNT', payload: 1 }); // Listener will not be called

Output:
Count: 5
Count: 7

Explanation: The store is initialized with state { count: 0 }. The listener logs the count. Unsubscribing removes the listener, so subsequent dispatches don't trigger it.

Example 3: (Edge Case)

Input:
const reducer = (state = 0, action) => {
  if (action.type === 'ERROR') {
    throw new Error('Simulated error');
  }
  return state + action.payload;
};

const store = MiniState.createStore(reducer);

store.subscribe((state) => console.log('State:', state));

store.dispatch({ type: 'ADD_VALUE', payload: 10 });
store.dispatch({ type: 'ERROR' });

Output:
State: 10
(Error: Simulated error - logged to console, state remains 10)

Explanation: The reducer throws an error when the action type is 'ERROR'. The state remains unchanged, and the error is logged to the console.

Constraints

  • The library should be implemented in plain JavaScript (no external libraries).
  • The reducer function must be pure.
  • The subscribe method must return a function that, when called, removes the listener.
  • The getState method must return the current state.
  • The dispatch method must call the reducer with the current state and the action.
  • The code should be reasonably well-structured and readable.
  • Performance is not a primary concern for this simplified implementation.

Notes

  • Focus on the core functionality of state management. You don't need to implement advanced features like middleware or time-travel debugging.
  • Consider using closures to encapsulate the store's state and listeners.
  • Think about how to handle multiple subscriptions and ensure that listeners are correctly removed when unsubscribed.
  • This is a good opportunity to practice your JavaScript fundamentals, including functions, closures, and object-oriented programming concepts.
  • Start with a simple reducer and gradually add complexity as you go.
  • Test your code thoroughly to ensure that it behaves as expected in various scenarios.
Loading editor...
javascript