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:
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.Storeobject: 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 anUnsubscribeFunctionwhich, 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.
Reducer<State>: A type representing a reducer function. It takes the current state and an action and returns a new state.State: A type representing the application's state. This is intentionally generic to allow for any state structure.Action: A type representing an action. This is intentionally generic to allow for any action structure.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
createStorefunction should initialize a store with an initial state ofundefined. getStateshould return the current state.subscribeshould call the listener function immediately with the initial state and then again whenever the state changes.dispatchshould 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.unsubscribeshould 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
undefinedornull. (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
dispatchfunction 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.