Simple, unopinionated, lightweight and extensible state management for Vue 3. Take a look at the demo to see it in action or play around with it in CodeSandbox.
Harlem has a simple functional API for creating, reading and mutating state. At it’s heart, Harlem just uses Vue reactive objects and computeds which means if you know how to use Vue, you’ll know how to use Harlem.
Harlem doesn’t impose any standards or conventions on your codebase. Because of it’s simple functional API you can structure your code anyway you want and Harlem will just work.
All state provided from a Harlem store is immutable by default. The only write access to state is through mutations. This ensures all updates to your store are tracable, thereby reducing the amount of bugs produced by code unpredictably mutating state.
Harlem weighs in at around 1KB (minified & gzipped) which makes it the perfect solution for codebases of all sizes. Harlem is also designed to be tree-shakable - unused stores, getters, or mutations will be removed from your code at build time (provided you are using a build tool that supports tree-shaking).
It’s also worth noting that Harlem has zero dependencies (apart from Vue obviously).
Harlem uses a plugin architecture so you can extend it any way you want. Some of the official plugins include Vue devtools integration, local/session storage sync, and transactions for rolling back multiple mutations when write errors occur.
Harlem has a great developer experience. It’s built using TypeScript so all of your state, getters, and mutations are strongly typed. Harlem also has devtools integration so you can explore your stores and see mutation events on the timeline in realtime.
Disclaimer: Harlem is currently experimental and subject to change as the Vue 3 ecosystem continues to stabilise. It is not recommended to use Harlem in critical client-facing production software at this stage. Once Harlem is considered stable for production applications the version will be bumped from patch increments to a minor increment (eg. will move from version 1.0.x to version 1.x.0) which may introduce breaking changes.
Getting started is simple:
@harlem/core
and any plugins you wish to include (I recommend installing @harlem/plugin-devtools
during development):npm install @harlem/core
Or if you’re using Yarn:
yarn add @harlem/core
import App from './app.vue';
import Harlem from '@harlem/core';
createApp(App)
.use(Harlem)
.mount('#app');
import {
createStore
} from '@harlem/core';
const STATE = {
firstName: 'John',
lastName: 'Smith'
};
const {
getter,
mutation,
...store
} = createStore('user', STATE);
export const state = store.state;
export const fullName = getter('fullname', state => `${state.firstName} ${state.lastName}`);
export const setFirstName = mutation('setFirstName', (state, payload) => {
state.firstName = payload || '';
});
export const setLastName = mutation('setLastName', (state, payload) => {
state.lastName = payload || '';
});
<template>
<div class="app">
<h1>Hello {{ fullName }}</h1>
<input type="text" v-model="firstName" placeholder="First name">
<input type="text" v-model="lastName" placeholder="Last name">
</div>
</template>
<script lang="ts">
import {
defineComponent,
computed
} from 'vue';
import {
state,
fullName,
setFirstName,
setLastName
} from './stores/user';
export default defineComponent({
setup() {
const firstName = computed({
get: () => state.firstName,
set: setFirstName
});
const lastName = computed({
get: () => state.lastName,
set: setLastName
});
return {
firstName,
lastName,
fullName
};
}
});
</script>
Enabling devtools support is, you guessed it, simple. Just import @harlem/plugin-devtools
and register it with your Harlem plugin:
import App from './app.vue';
import Harlem from '@harlem/core';
import createDevtoolsPlugin from '@harlem/plugin-devtools';
import {
createApp
} from 'vue';
function start() {
let plugins = [];
if (process.env.NODE_ENV === 'development') {
plugins.push(createDevtoolsPlugin({
label: 'State'
}));
}
return createApp(App)
.use(Harlem, {
plugins
})
.mount('#app');
}
start();
See the devtools plugin docs for more information on the options available.
At the time of writing this you will need to use the Beta version of the Vue devtools.
Harlem supports using stores in an SSR application via the SSR plugin (@harlem/plugin-ssr
). Refer to the SSR plugin documentation for more information and how to get started. The SSR plugin docs are available here.
Harlem is completely extensible through plugins. Feel free to choose from some of the official plugins or write your own. See the plugins documentation from more information on the official set of plugins or how to author your own plugin.
Some of the official plugins include:
@harlem/plugin-devtools
) - The devtools plugin adds Vue devtools integration with your stores to show updates to your state in realtime.@harlem/plugin-reset
) - The reset plugin provides an API to reset stores to their initial state.@harlem/plugin-snapshot
) - The snapshot plugin provides an API to snapshot a store’s state at a given point and apply it when convenient.@harlem/plugin-ssr
) - The SSR plugin enables support for using Harlem stores in a server-side rendered application.@harlem/plugin-storage
) - The storage plugin provides simple local/session storage synchronisation with your state. This plugin relieves the burden of having to manually save your state to a web storage resource.@harlem/plugin-transaction
) - The transaction plugin provides an API for defining transactions that run multiple mutations. A transaction can safely rollback mutations in the event of an error.Harlem fully supports Typescript - just decorate your mutation with the payload type and Harlem will take care of the rest:
export const setFirstName = mutation<string>('setFirstName', (state, payload) => {
state.firstName = payload || ''
});
All other types (state, getters etc) are automatically inferred, however should you wish to define your own state type you can do so during store creation:
import {
createStore
} from '@harlem/core';
interface State {
firstName?: string;
lastName?: string;
};
const STATE: State = {
firstName: 'John',
lastName: 'Smith'
};
const {
getter,
mutation,
...store
} = createStore<State>('user', STATE);
In most cases this will be unnecessary but it can be useful for defining nullable fields or fields that don’t exist at the time of store creation.
Harlem doesn’t provide a mechanism for actions - this is by design. Actions are commonly asynchronous methods that contain business logic which group a single mutation or set of mutations together. Harlem leaves your action design up to you. Here is a simple example of an action using Harlem:
import {
setLoading,
setUserDetails
} from './mutations';
export async function loadUserDetails(userId) {
setLoading(true);
try {
const response = await fetch(`/api/users/${userId}`);
const data = await response.json();
setUserDetails(data);
} finally {
setLoading(false);
}
}
Certainly - just import the state or getter from one store into the getter you are authoring on another store. For example:
import {
state as otherState
} from '../other-store;
import {
getter
} from './store';
export const myNumberGetter = getter('myNumber', state => state.myNumber + otherState.otherNumber);
This also works for importing getters from other stores. Just remember that to access the value of a getter you will need to use the .value
property of the getter. For example, if I had a getter name myGetter
and I wanted to use it in another getter I would have to use myGetter.value
to access it’s raw value.
See the Vue documentation on computeds for more information. Vue Computed.
Short answer, no. Because Harlem attempts to be as unonpinionated as possible that means it’s up to you to structure your store how you see fit. That being said here are 2 examples that may give you a headstart:
- stores
- store1
state.js
getters.js
mutations.js
actions.js
store.js
index.js
- store2
state.js
getters.js
mutations.js
actions.js
store.js
index.js
- stores
- store1
- getters
getter-1.js
getter-2.js
- mutations
mutation-1.js
mutation-2.js
- actions
action-1.js
action-2.js
state.js
store.js
index.js
- store2
- getters
getter-1.js
getter-2.js
- mutations
mutation-1.js
mutation-2.js
- actions
action-1.js
action-2.js
state.js
store.js
index.js
In both cases the store.js
file and the index.js
files would look roughly the same.
// store.js
import STATE from './state';
import {
createStore
} from '@harlem/core';
export const {
state,
getter,
mutation,
...store
} = createStore('store1', STATE);
// index.js - single file structure
export { state } from './store';
export {
getter1,
getter2
} from './getters';
export {
mutation1,
mutation2
} from './mutations';
// index.js - multi-file structure
export { state } from './store';
export { default as getter1 } from './getters/getter-1';
export { default as getter2 } from './getters/getter-2';
export { default as mutation1 } from './mutations/mutation-1';
export { default as mutation2 } from './mutations/mutation-2';
Author: andrewcourtice
Demo: https://harlemjs.com/
Source Code: https://github.com/andrewcourtice/harlem
#vue #vuejs #javascript