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:
-
createStore(reducer): This function takes a single argument: areducerfunction. Thereduceris a pure function that takes the current state and an action as input and returns the new state. ThecreateStorefunction should return an object with the following methods:getState(): Returns the current state.dispatch(action): Dispatches an action to the store. This should call thereducerwith the current state and the action, and update the internal state accordingly.subscribe(listener): Takes alistenerfunction as an argument. This function should be called whenever the state changes. Thelistenershould receive the new state as an argument. Thesubscribemethod should return an unsubscribe function. Calling the unsubscribe function should remove the listener from the list of subscribers.
-
Actions: Actions are plain JavaScript objects that represent events that occur in the application. They are passed to the
dispatchmethod. While the action object itself doesn't have a strict format, it's common practice to include atypeproperty. -
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 alwaysundefined.)
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
reducerfunction must be pure. - The
subscribemethod must return a function that, when called, removes the listener. - The
getStatemethod must return the current state. - The
dispatchmethod 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.