React State Management Library Challenge
This challenge asks you to build a foundational state management library for React applications using TypeScript. You will create a system that allows components to subscribe to global state changes, update that state, and trigger re-renders when the state they depend on changes. This is a core concept for building complex and scalable React applications without relying on external libraries.
Problem Description
Your task is to implement a simple, yet effective, state management solution in TypeScript for React. This solution should provide a way to:
- Define and initialize global state: A single source of truth for application data.
- Access state within components: Components should be able to read specific parts of the global state.
- Update state: Provide mechanisms to modify the global state immutably.
- Subscribe to state changes: Components should re-render automatically when the relevant parts of the global state change.
Key Requirements:
createStore(initialState): A function that takes an initial state object and returns a store object.- The store object should have a
getState()method to retrieve the current state. - The store object should have a
setState(updater)method to update the state. Theupdatercan be either a new state object or a function that receives the current state and returns the new state. This ensures immutability. - The store object should have a
subscribe(callback)method that registers a callback function to be executed whenever the state changes. It should return anunsubscribefunction to remove the callback.
- The store object should have a
useStore(store, selector)hook: A React hook that allows components to subscribe to state changes.- It takes the
storeobject (created bycreateStore) and an optionalselectorfunction. - The
selectorfunction receives the current state and should return the specific piece of state the component is interested in. If no selector is provided, the entire state is returned. - The hook should return the selected state.
- When the selected state changes, the component using the hook should re-render.
- The hook must handle multiple components subscribing to the same store and different parts of the state.
- Ensure that only components whose selected state has actually changed are re-rendered.
- It takes the
Expected Behavior:
When the global state is updated using setState, all subscribed components should be notified. Components whose selected state (as determined by the selector function) has changed should re-render. Components whose selected state has not changed should not re-render.
Edge Cases:
- No selector provided to
useStore: The hook should correctly return the entire state. - Multiple components subscribing to the same part of the state.
- Multiple components subscribing to different parts of the state.
- State updates that result in no actual change to the selected state for a component.
- Unsubscribing from the store.
Examples
Example 1: Basic State Access and Update
// Assume createStore and useStore are implemented correctly
// Initial state
const initialState = { count: 0, message: "Hello" };
// Create the store
const myStore = createStore(initialState);
// Component A (subscribes to the entire state)
function ComponentA() {
const state = useStore(myStore); // No selector, gets entire state
return <div>Count: {state.count}, Message: {state.message}</div>;
}
// Component B (subscribes to a specific part of the state)
function ComponentB() {
const count = useStore(myStore, (state) => state.count); // Selects 'count'
return <div>Current Count: {count}</div>;
}
// --- Usage ---
// Initial render:
// ComponentA shows: Count: 0, Message: Hello
// ComponentB shows: Current Count: 0
// After calling myStore.setState({ count: 1 });
// ComponentA shows: Count: 1, Message: Hello (re-renders)
// ComponentB shows: Current Count: 1 (re-renders)
// After calling myStore.setState({ message: "World" });
// ComponentA shows: Count: 1, Message: World (re-renders)
// ComponentB shows: Current Count: 1 (does NOT re-render as 'count' hasn't changed)
Example 2: State Update with Updater Function
// Assume createStore and useStore are implemented correctly
const initialState = { counter: { value: 10 } };
const counterStore = createStore(initialState);
function CounterDisplay() {
const value = useStore(counterStore, (state) => state.counter.value);
return <div>Counter: {value}</div>;
}
function CounterButton() {
const increment = () => {
counterStore.setState(prevState => ({
counter: {
...prevState.counter, // Important for immutability
value: prevState.counter.value + 1
}
}));
};
return <button onClick={increment}>Increment</button>;
}
// --- Usage ---
// Initial render:
// CounterDisplay shows: Counter: 10
// Clicking the button:
// 1. Increment: CounterDisplay shows: Counter: 11
// 2. Increment: CounterDisplay shows: Counter: 12
Example 3: Unsubscribing
// Assume createStore and useStore are implemented correctly
const initialState = { data: "initial" };
const dataStore = createStore(initialState);
function DataDisplay() {
const data = useStore(dataStore, (state) => state.data);
return <div>Data: {data}</div>;
}
function App() {
const [showDisplay, setShowDisplay] = React.useState(true);
return (
<div>
{showDisplay && <DataDisplay />}
<button onClick={() => setShowDisplay(false)}>Hide Display</button>
<button onClick={() => dataStore.setState({ data: "updated" })}>Update Data</button>
</div>
);
}
// --- Usage ---
// Initial render: DataDisplay shows: Data: initial
// Clicking "Update Data": DataDisplay shows: Data: updated
// Clicking "Hide Display": DataDisplay component unmounts.
// The subscription associated with DataDisplay should be automatically cleaned up.
// Clicking "Update Data" again should not cause errors or try to update an unmounted component's state.
Constraints
- TypeScript Version: Use TypeScript 4.0 or later.
- React Version: Assume React 17 or later.
- No External State Management Libraries: Do not use libraries like Redux, Zustand, Jotai, Recoil, etc. Your solution must be built from scratch.
- Immutability: State updates must be immutable. Directly mutating the state object is not allowed.
- Performance: The
useStorehook should only trigger re-renders when the selected state actually changes. Avoid unnecessary re-renders. - Dependencies: Your solution should not introduce any dependencies beyond React itself and its core utilities.
Notes
- Consider how you will manage the list of subscribers and how to efficiently notify only those whose relevant state has changed.
- The
selectorfunction is crucial for performance optimization. Think about how to compare the previous selected value with the new one to determine if a re-render is necessary. - Remember to handle component unmounting and unsubscribe listeners to prevent memory leaks.
- This is a simplified state management library. Real-world libraries often have more advanced features (middleware, dev tools integration, etc.), but focus on the core functionality described here.