Store
Onchange and Effects

onChange and effects

Davstack Store provides onChange and effects to manage side effects and state changes in a predictable way.

It is important to understand how to use these features TOGETHER to create a robust state management system.

tldr: you can use onChange to subscribe to state changes and effects to encapsulate side effects.

Usage example

import { store } from '@davstack/store';
 
const userStore = store({
	name: 'John',
	age: 25,
}).effects((store) => ({
	logChanges: () => store.onChange(console.log),
}));

onChange Method

The onChange method callback is called whenever the state changes and can be used to trigger side effects. This is useful for scenarios like logging, syncing with external systems, or complex state-driven effects.

Usage

Subscribe to state changes with onChange. It returns an unsubscribe function to prevent memory leaks.

const countStore = store(0);
const unsubscribe = countStore.onChange((newVal, oldVal) =>
	console.log(`Changed from ${oldVal} to ${newVal}`)
);

Selective State Changes

Use onChange on specific store segments to only run the callback when those parts change.

const unsubscribe = userStore.name.onChange(console.log);

Dependencies (deps)

Specify deps to react only to certain state parts. It can be a keys array or a function returning dependencies.

const unsubscribe = store.onChange(callback, { deps: ['name '] });
const unsubscribe = store.onChange(callback, {
	deps: (state) => [state.name],
});

You can subscribe to deeply nested changes inside the deps callback eg state => [state.user.address.street]

Immediate Invocation

Use fireImmediately to trigger the callback immediately with the current state.

const unsubscribe = countStore.onChange(console.log, { fireImmediately: true });

Custom Equality Checker

Control callback invocation with a custom equality function. If it returns true, the callback does not fire.

const unsubscribe = countStore.onChange(console.log, {
	equalityChecker: (newState, oldState) =>
		newState.someValue === oldState.someValue,
});

Effects

The store.effects method allows you to encapsule the logic related to state changes, making it easily testable and reusable.

They enable you to bind logic to the specific store instance, which makes it possible to have multiple instances of the store (see local state management)

Defining and Using Effects

Define effects within the store by returning a object similar to actions but each callback should use the .onChange method to subscribe to state changes.

const countStore = store(0).effects((store) => ({
	logChanges: () => store.onChange(console.log),
}));

Effect Methods and Subscriptions

The effects are automatically subscribed to when the store is created, so it is unlikely that you will need to use this.

However, you can manually access the effects eg for testing using store._effects.effectName().

const unsub = countStore._effects.logChanges();

Additionally, you can subscribe/unsubscribe from all of the store's effects

// subscribe to all the stores effects
countStore.subscribeToEffecs();
 
// unsubscribe from all the stores effects
countStore.unsubscribeFromEffects();

These methods are used under the hood of createStoreContext to make multiple instances of the store work correctly.

Here is a super simplified version of createStoreContext to show how the effects are unsubscribed from when the component is unmounted

export function createStoreContext(store) {
	const Provider = () => {
		const storeInstance = React.useRef(store.create(localInitialValue));
 
		React.useEffect(() => {
			return () => {
				storeInstance.current.unsubscribeFromEffects();
			};
		}, []);
 
		return (
			<Context.Provider value={storeInstance.current}>
				{children}
			</Context.Provider>
		);
	};
}