Implement Observable / Reactive Store
Interview Question: "Implement a simple reactive store where state changes automatically notify subscribers."
Level 1: Basic Observable Store
function createStore(initialState) {
let state = initialState;
const listeners = new Set();
return {
getState: () => state,
setState: (updater) => {
const nextState = typeof updater === 'function'
? updater(state)
: { ...state, ...updater };
if (nextState !== state) {
state = nextState;
listeners.forEach(fn => fn(state));
}
},
subscribe: (listener) => {
listeners.add(listener);
return () => listeners.delete(listener);
},
};
}Level 2: With Selectors (Zustand-like)
function createStore(initializer) {
let state;
const listeners = new Set();
const get = () => state;
const set = (partial) => {
const nextState = typeof partial === 'function' ? partial(state) : partial;
if (Object.is(nextState, state)) return;
const previousState = state;
state = typeof nextState === 'object'
? { ...state, ...nextState }
: nextState;
listeners.forEach(fn => fn(state, previousState));
};
state = initializer(set, get);
function subscribe(selector, callback) {
if (!callback) {
callback = selector;
selector = (s) => s;
}
let currentSlice = selector(state);
const listener = (nextState) => {
const nextSlice = selector(nextState);
if (!Object.is(currentSlice, nextSlice)) {
const prev = currentSlice;
currentSlice = nextSlice;
callback(currentSlice, prev);
}
};
listeners.add(listener);
return () => listeners.delete(listener);
}
return { getState: get, setState: set, subscribe };
}
// Usage
const store = createStore((set, get) => ({
count: 0,
users: [],
increment: () => set({ count: get().count + 1 }),
addUser: (user) => set({ users: [...get().users, user] }),
}));
store.subscribe(
(s) => s.count,
(count) => console.log('Count changed:', count)
);
store.getState().increment(); // "Count changed: 1"
store.getState().addUser({ name: 'Alice' }); // (no log â count didn't change)Key points:
- Selector-based subscriptions â Only notifies when the selected slice changes
Object.iscomparison â Same identity check React uses- Functional updates â
setaccepts both objects and updater functions - Initializer pattern â
setandgetare passed to the state factory (Zustand pattern)
Level 3: Proxy-Based Reactivity (Vue-like)
function reactive(target, onChange) {
const handler = {
get(obj, prop) {
const value = obj[prop];
if (typeof value === 'object' && value !== null) {
return reactive(value, onChange);
}
return value;
},
set(obj, prop, value) {
const oldValue = obj[prop];
if (Object.is(oldValue, value)) return true;
obj[prop] = value;
onChange(prop, value, oldValue);
return true;
},
};
return new Proxy(target, handler);
}
// Usage
const state = reactive({ count: 0, user: { name: 'Alice' } }, (prop, newVal) => {
console.log(`${String(prop)} changed to ${newVal}`);
});
state.count = 1; // "count changed to 1"
state.user.name = 'Bob'; // "name changed to Bob"Follow-Up: "How does this compare to Redux/Zustand?"
"Redux uses action dispatching with reducers â explicit updates with full action history. Zustand simplifies this with direct mutations and selector-based subscriptions. My implementation is closest to Zustand's model. The key architectural difference is that Proxy-based reactivity (Vue) tracks access automatically, while selector-based (Zustand/Redux) requires explicit subscription declarations."