nano-state
Efficient, effective, and extensible state management: less is more.
installation
npm i @sophosoft/nano-state
# or
yarn add @sophosoft/nano-state
usage
nano-state
is built from the ground-up with TypeScript to leverage strong typing. The target ECMAScript version is es5
. While the examples below may be contrived, they should illustrate normal usage.
state
Extend the State
type to define interfaces for your various states:
import { State } from '@sophosoft/nano-state'
interface UserState extends State {
displayName: string
email: string
id?: string
visits: number
}
context
A Context
provides exclusive managment of state. Implement getters and mutation methods as necessary in the context.
import { Context } from '@sophosoft/nano-state'
class UserContext extends Context<UserState> {
public constructor() {
// initialize default state
super({
displayName: 'Guest',
email: '',
visits: 0
})
}
public get DisplayName(): string {
return this.state.displayName
}
// ...
public login(id: string, name: string, email: string, visits: number): void {
this.state.displayName = name
this.state.email = email
this.state.id = id
this.state.visits = visits + 1
this.notify('User:LoggedIn', { id: this.state.id })
}
public async cache(manager: CacheManager): Promise<void> {
await manager.write('user', this.state)
this.notify('User:Cached')
}
// ...
}
Note the usage of
this.notify(string, payload?)
. This is the internal event system that is described below.
subscription
The Subscription
abstract class allows middleware to react to one or more events. The contract includes one async handle(event: Event<EventPayload>)
method, however a strategy pattern may be used to handle multiple events when necessary. By default, a subscription will listen to all events using the wildcard (*
) event id. Events are described next.
import { Event, EventPayload, Subscription } from '@sophosoft/nano-state'
class LogSubscription extends Subscription {
public async handle(event: Event<EventPayload>): Promise<void> {
console.log(event)
}
}
import { Subscription } from '@sophosoft/nano-state'
class AuthNSubscription extends Subscription {
public constructor() {
super(['AuthN:LogIn', 'AuthN:LogOut'])
}
public async handle(event: AuthNEvent): Promise<void> {
if (event.id === 'AuthN:LogIn') {
return this.handleLogIn(event)
} else if (event.id === 'AuthN:LogOut') {
return this.handleLogOut(event)
}
}
// ...
}
event
Extend the Event
and EventPayload
types to define specific event payloads.
Events contain an id
, store
, and an optional payload
. The Store
is descibed next and may be used by subscriptions to access various contexts and trigger additional events. By default, the payload may be undefined, which will cause syntax errors if payload properties are accessed without proper typing.
import { Event, EventPayload } from '@sophosoft/nano-state'
interface UserLoginPayload extends EventPayload {
id: string
name: string
email: string
visits: number
}
interface UserLoginEvent extends Event<UserLoginPayload> {
payload: UserLoginPayload
}
store
The Store
class provides access to all of the contexts in an application and dispatches events to all subscriptions. It is intended to be a singleton, globally accessible throughout an application.
Contexts are mapped by aliases, which allow third-party libraries to be integrated with domain-specific names. In order to minimize the inital overhead of registering multiple Contexts, a factory method can be used to lazy-load a Context only when it is requested via Store.getContext()
. In this way, a Context is not instanciated until it's needed, and then only once: The resulting instance is stored for subsequent requests.
When an event is triggered, subscriptions are notified in the order in which they were registered.
import { Store, ContextMap, Subscription } from '@sophosoft/nano-state'
// perhaps imported from a config file
const contexts: ContextMap = {
'authn': new AuthNContext(),
'user': () => new UserContext(), // lazy-loading
// ...
}
// perhaps also imported
const subscriptions: Subscription[] = [
new LogSubscription(),
new AuthNSubscription(),
// ...
]
class AppStore extends Store {
public static readonly Instance: AppStore = new AppStore()
private constructor() {
super(contexts, subscriptions)
}
}
Using the store within an application is then relatively trivial.
import { AppStore } from '@/store/AppStore'
import { UserContext } from '@/user/UserContext'
const store: AppStore = AppStore.Instance
const user: UserContext = store.getContext<UserContext>('user')
console.debug(`${user.DisplayName} is the current user`)
store.trigger('AuthN:LogOut', { id: user.Id })