Lightweight state management core with adapters for React, Vue, Svelte, and Vanilla.
@ga-ut/store-core
(framework-agnostic store and types)@ga-ut/store-react
(useStore
hook)@ga-ut/store-vue
(toRef
, select
, bindMethods
)@ga-ut/store-svelte
(toReadable
, select
, bindMethods
)@ga-ut/store-vanilla
(watch
, on
, select
, toObservable
)Install only what you need. Examples below show per-framework usage.
@ga-ut/store-core
)Create a strongly-typed store with state and actions. Assign to this
to update state. Subscribe to granular changes when needed.
import { Store } from '@ga-ut/store-core'
const counter = new Store({
count: 0,
inc() { this.count += 1 },
dec() { this.count -= 1 }
})
// Read/write
counter.getState().inc()
// Subscribe (receives modified keys)
const unsubscribe = counter.subscribe((modified) => {
if (modified.has('count')) {
console.log('count changed to', counter.getState().count)
}
})
unsubscribe()
@ga-ut/store-react
)Fine-grained re-renders using useStore
. Components only re-render for the state keys they read.
import { Store } from '@ga-ut/store-core'
import { useStore } from '@ga-ut/store-react'
const countStore = new Store({
count: 0,
inc() { this.count += 1 },
dec() { this.count -= 1 }
})
function Counter() {
const { count } = useStore(countStore)
return <p>Count: {count}</p>
}
function Controls() {
const { inc, dec } = useStore(countStore)
return (
<>
<button onClick={inc}>+</button>
<button onClick={dec}>-</button>
</>
)
}
@ga-ut/store-vue
)Convert slices to Vue refs and bind actions for Composition API.
// <script setup lang="ts">
import { shallowRef } from 'vue'
import { Store } from '@ga-ut/store-core'
import { toRef, bindMethods } from '@ga-ut/store-vue'
const store = new Store({
count: 0,
inc() { this.count += 1 }
})
const { ref: count } = toRef(store, s => s.count, shallowRef)
const { inc } = bindMethods(store) as { inc: () => void }
// </script>
// <template>
// <button @click="inc">+</button>
// <span>{{ count }}</span>
// </template>
Selecting multiple values into a single ref:
import { shallowRef } from 'vue'
import { select } from '@ga-ut/store-vue'
const { ref: tuple } = select(store, [ s => s.count, s => s.other ], shallowRef)
@ga-ut/store-svelte
)Expose slices as readable stores and bind actions.
<!-- Counter.svelte -->
<script>
import { toReadable, bindMethods } from '@ga-ut/store-svelte'
export let store
const count$ = toReadable(store, s => s.count) // readable
const { inc } = bindMethods(store)
$: doubled = $count$ * 2
// use $count$ in markup (auto-subscribe)
</script>
<button on:click={inc}>+</button>
<div>{ $count$ } / { doubled }</div>
@ga-ut/store-vanilla
)Framework-free helpers for subscriptions and interop.
import { Store } from '@ga-ut/store-core'
import { watch, on, select, toObservable } from '@ga-ut/store-vanilla'
const s = new Store({
value: 0,
set(v: number) { this.value = v }
})
// watch a selector
const unWatch = watch(s, x => x.value, v => console.log('value:', v), { fireImmediately: true })
// explicit keys
const unOn = on(s, ['value'], () => console.log('value changed'))
// tiny derived view
const view = select(s, x => x.value)
const unView = view.subscribe(v => console.log('derived:', v))
// minimal observable interop
const obs = toObservable(s, x => x.value)
const unObs = obs.subscribe({ next: v => console.log('obs:', v) })
unWatch(); unOn(); unView(); unObs()
This lightweight state management solution provides convenient syntax, strong type safety, and rendering control through framework adapters.
Creating and using a store is straightforward, with no need for separate setters. Simply assign values to this within the store, and pass the store to components that need rendering.
Example (React):
import { Store } from '@ga-ut/store-core';
import { useStore } from '@ga-ut/store-react';
const countStore = new Store({
count: 0,
increment() {
this.count += 1;
},
decrement() {
this.count -= 1;
}
});
function Counter() {
const { count, increment, decrement } = useStore(countStore);
return (
<div>
<p>Count: {count}</p>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
The store provides strong type safety for state access and mutations:
// ❌ Type Error: Cannot access non-existent property
const store = new Store({ count: 0 });
store.getState().nonexistent; // Error: Property 'nonexistent' does not exist
// ❌ Type Error: Cannot assign wrong type
store.getState().count = "1"; // Error: Type 'string' is not assignable to type 'number'
// ❌ Type Error: Cannot reference other methods inside a method
const store = new Store({
count: 0,
increment() {
this.count += 1;
},
double() {
this.increment(); // Error: Property 'increment' does not exist on type '{ count: number }'
this.count *= 2;
}
});
// ✅ Correct: Methods should be self-contained
const store = new Store({
count: 0,
increment() {
this.count += 1;
},
double() {
this.count *= 2;
}
});
The store automatically optimizes rendering performance. Components will only re-render when their specifically accessed store values change, ensuring efficient updates.
Example (React rendering behavior):
function Counter() {
// Re-renders only when 'count' changes
const { count } = useStore(countStore);
return <p>Count: {count}</p>;
}
function Controls() {
// Re-renders when any accessed state changes
const { increment, decrement } = useStore(countStore);
return (
<div>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
);
}
// Getter functions are also optimized
const statsStore = new Store({
numbers: [1, 2, 3],
getMax() {
// Re-renders only when 'numbers' array changes
return Math.max(...this.numbers);
},
addNumber(num) {
this.numbers = [...this.numbers, num];
}
});
function Stats() {
const { getMax, addNumber } = useStore(statsStore);
return (
<div>
<p>Maximum: {getMax()}</p>
<button onClick={() => addNumber(4)}>Add 4</button>
</div>
);
}
In the example above, the Stats
component will only re-render when the numbers
array changes, even though it's using a getter function. The store automatically tracks dependencies used within getter functions and optimizes rendering accordingly.
You can integrate with Immer to handle immutable updates of complex state structures.
Example:
import { Store } from '@ga-ut/store-core';
import { useStore } from '@ga-ut/store-react';
import { produce } from 'immer';
interface Address {
city: string;
zip: string;
}
interface Contact {
email: string;
phone: string;
}
interface Person {
name: string;
age: number;
address: Address;
contact: Contact;
updateUserProfile(newCity: string, newPhone: string): void;
}
const personStore = new Store<Person>({
name: 'Alice',
age: 25,
address: {
city: 'New York',
zip: '10001'
},
contact: {
email: '[email protected]',
phone: '123-456-7890'
},
updateUserProfile(newCity: string, newPhone: string) {
this.address = produce(this.address, (draft) => {
draft.city = newCity;
});
this.contact = produce(this.contact, (draft) => {
draft.phone = newPhone;
});
}
});
function UserProfile() {
const { address, contact, updateUserProfile } = useStore(personStore);
return (
<>
<span>{address.city}</span>
<span>{contact.phone}</span>
<button onClick={() => updateUserProfile('Seoul', '012-345-6789')}>
Update Profile
</button>
</>
);
}
For more detailed examples and API documentation, please refer to the source code and test files.
import { Store } from '@ga-ut/store-core'
import { toRef, bindMethods } from '@ga-ut/store-vue'
const store = new Store({ count: 0, inc() { this.count += 1 } })
const { ref: count } = toRef(store, s => s.count, (v) => ({ value: v }))
const { inc } = bindMethods(store)
import { Store } from '@ga-ut/store-core'
import { toReadable, bindMethods } from '@ga-ut/store-svelte'
const store = new Store({ count: 0, inc() { this.count += 1 } })
const count$ = toReadable(store, s => s.count)
const { inc } = bindMethods(store)
import { Store } from '@ga-ut/store-core'
import { watch } from '@ga-ut/store-vanilla'
const store = new Store({ count: 0, inc() { this.count += 1 } })
const un = watch(store, s => s.count, (value) => console.log('count', value))