Code Monkey home page Code Monkey logo

harlem's Introduction

Harlem

Harlem

Build and Test npm

Powerfully simple global state management for Vue 3. Head over to harlemjs.com to get started or take a look at the demo to see it in action.

Foundations

Simple

Harlem has a simple functional API for creating, reading and mutating state. From the most basic global state management needs to the most complex, Harlem has you covered.

Safe

All state exposed from a Harlem store is immutable. State can only be changed through mutations/actions making your state predictable and auditable.

Modular

Harlem is super lightweight, tree-shakeable, and dependency-free! It is designed to be used with native ES modules so even unused parts of your stores (getters, mutations, actions etc.) can be tree-shaken.

Extensible

Harlem comes with a suit of official extensions that allow you to add extra features to your stores such as cancellable actions, tracing, transactions, undo/redo, and more. You can even write your own extension or plugin.

Great DX

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 Vue devtools integration so you can explore your stores and see store events on the timeline in realtime.

Battle-Tested

Harlem is built by enterprise software engineers and used in medium-large enterprise software. It's built to handle even the most complex state management use-cases.

Features

  • TypeScript support
  • Vue devtools integration
  • Lightweight & dependency-free
  • Tree-shakeable
  • Extensible (via plugins & extensions)
  • SSR Support

Check out the docs for more details.

Getting Started

Getting started with Harlem is easy. Just follow the steps below and you'll be up and running in no time.

Installation

Install harlem and any plugins/extensions you wish to include.

# yarn
yarn add harlem

# npm
npm install harlem

The devtools plugin is enabled by default during development and tree-shaken out of production builds. If you don't need devtools during develpment, you can instead install harlem from @harlem/core

If you're using Nuxt, instead follow the instructions to install the Nuxt module and then resume this guide below, at Create your first store.

Register the Harlem Vue plugin

Register the Harlem plugin with your Vue app instance:

import App from './app.vue';

import {
    createVuePlugin
} from 'harlem';

createApp(App)
    .use(createVuePlugin())
    .mount('#app');

Create your first store

Create your store and define any getters, actions or mutations:

import {
    createStore
} from 'harlem';

// The initial state for this store
const STATE = {
    firstName: 'John',
    lastName: 'Smith'
};

// Create the store, specifying the name and intial state
export const {
    state, 
    getter,
    mutation,
    action,
    ...store
} = createStore('user', STATE);

export const fullName = getter('fullname', state => `${state.firstName} ${state.lastName}`);

export const setFirstName = mutation('set-first-name', (state, payload: string) => {
    state.firstName = payload;
});

export const setLastName = mutation('set-last-name', (state, payload: string) => {
    state.lastName = payload;
});

export const loadDetails = action('load-details', async (id: string, mutate) => {
    const response = await fetch(`/api/details/${id}`);
    const details = await response.json();

    mutate(state => {
        state.details = details;
    });
});

Use your store in your app

To use your store in your app just import the parts of it you need.

<template>
    <div class="app">
        <h1>Hello {{ fullName }}</h1>
        <button @click="loadDetails()">Load Details</button>
        <input type="text" v-model="firstName" placeholder="First name">
        <input type="text" v-model="lastName" placeholder="Last name">
    </div>
</template>

<script lang="ts" setup>
import {
    computed
} from 'vue';

import {
    state,
    fullName,
    setFirstName,
    setLastName,
    loadDetails
} from './stores/user';

const firstName = computed({
    get: () => state.firstName,
    set: value => setFirstName(value)
});

const lastName = computed({
    get: () => state.lastName,
    set: value => setLastName(value)
});
</script>

Extensibility

Harlem uses a combination of extensions and plugins to extend core functionality.

Extensions

Extensions are per-store additions to Harlem's core functionaility. Extensions are often used for adding store features, changing store behaviour and various other low-level tasks. This is the primary method in which Harlem stores are extended. Feel free to choose from some of the official extensions or write your own. See the extensions documentation from more information on the official set of extensions or how to author your own plugin.

The official extensions include:

  • Action (@harlem/extension-action) - Extends a store to support advanced actions (cancellation, status & error tracking etc.).
  • Compose (@harlem/extension-compose) - Extends a store to to add simple read/write convenience methods.
  • History (preview) (@harlem/extension-history) - Extends a store to support undo and redo capabilities.
  • Lazy (@harlem/extension-lazy) - Extends a store to support lazy async getters.
  • Storage (@harlem/extension-storage) - Extends a store to support synchronising state to/from localStorage or sessionStorage.
  • Trace (@harlem/extension-trace) - Extends a store to support tracing granular changes to state during mutations. Useful for auditing during development.
  • Transaction (@harlem/extension-transaction) - Extends a store to support rolling back multiple mutations if one fails.

Plugins

Plugins are global extensions to Harlem's core functionality. Plugins are often used for generic store operations like tracking events and collating state. 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.

The official plugins include:

  • Devtools (@harlem/plugin-devtools) - The devtools plugin adds Vue devtools integration with your stores to show updates to your state in realtime.
  • SSR (@harlem/plugin-ssr) - The SSR plugin enables support for using Harlem stores in a server-side rendered application.

Documentation

Full documentation for Harlem is available at https://harlemjs.com.

Credits

Logo design by Ethan Roxburgh

harlem's People

Contributors

andrewcourtice avatar binochoi avatar cfjedimaster avatar cperrin88 avatar danielroe avatar edimitchel avatar lexpeartha avatar ragokan avatar robertmoura avatar yaquawa avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

harlem's Issues

Module parse failed: Unexpected token (112:19)

I have a problem with Harlem when I use Vue CLI boilerplate.

The compilation error is following:

ERROR  Failed to compile with 1 error
error  in ./node_modules/@harlem/core/dist/esm/index.js
Module parse failed: Unexpected token (112:19)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See https://webpack.js.org/concepts#loaders
|       providers: {
|         ...PROVIDERS,
>         ...options?.providers
|       }
|     };

 @ ./src/main.js 7:0-34 8:19-25
 @ multi (webpack)-dev-server/client?http://192.168.1.69:8080&sockPath=/sockjs-node (webpack)/hot/dev-server.js ./src/main.js`

Steps to reproduce the behaviour:

  1. Use vue create app to create vue project.
  2. Install Harlem with NPM.
  3. Add import Harlem from '@harlem/core'; and createApp(App).use(Harlem).mount('#app') to main.js file.
  4. Run npm run serve to compile the project.
  5. Have aforementioned error.

Version:

  1. Harlem: ^2.0.0
  2. Vue: ^3.0.0
  3. "@vue/cli-plugin-babel": "~4.5.0",
  4. "@vue/cli-plugin-eslint": "~4.5.0",
  5. "@vue/cli-service": "~4.5.0",

Using Vue + Vite + Harlem with SSR

Describe the bug
I have a completed application. I am using Vue 3 + Vite + Harlem. Now I'm trying to set up SSR, but an error occurs.

$ node --trace-warnings ./server.js
http://localhost:3000
TypeError: Cannot read properties of undefined (reading 'createVuePlugin') 
    at /src/store/index.js:5:14
    at async instantiateModule (file:///C:/Projects/website/node_modules/vite/dist/node/chunks/dep-5e7f419b.js:52224:9)
$ cross-env NODE_ENV=production node --trace-warnings ./server.js
http://localhost:3000 
ReferenceError: window is not defined 
    at file:///C:/Projects/website/node_modules/@harlem/core/dist/index.bundler.mjs:481:1
    at ModuleJob.run (node:internal/modules/esm/module_job:194:25)

// store/index.js

import { createVuePlugin } from "@harlem/core";
import { createServerSSRPlugin } from "@harlem/plugin-ssr";
import storageExtension from "@harlem/extension-storage";

const store = createVuePlugin({
  plugins: [createServerSSRPlugin()],
});

export const storageExt = storageExtension({
  type: "local",
  prefix: "app",
  sync: true,
});

export default store;

Version (please complete the following information):

  • Harlem: 3.0.2
  • Vue: 3.2.38
  • Vite: 4.0.4
  • Express: 4.18.2
  • Node JS: 18.13.0

Allow mutations to take multiple parameters

I would like to be able to create mutations that can take multiple arguments. e.g.

import { createStore } from "@harlem/core";

const STATE = {
  map: new Map<string, string>(),
};

const { getter, mutation, ...state } = createStore("store", STATE);

const addItem = mutation("addItem", (state, key: string, value: string) => {
  state.map.set(key, value);
});

addItem("example", "harlem is awesome");

For my use case this would allow me to bind the first parameter to a fixed value and have a user button press set the value, without having to repeat the key every time.

I can't see any way/reason how this change would effect existing implementations, when e.g. implemented using the spread operator.

PS: thank you for generally awesome library

Bug when Vite HMR

This is a fix report and an enhancement and a plugin request all in one, so I decided to create a blank issue

I have encountered the same issue as here #24
I have seen your fix #25 and would say I am not really agree with meaning of fix

My concerns:

Code:

  1. This fix is more of a breaking change than a fix.
  2. It is not very good to have different behaviour depending on some variable from outside. What if (suddenly ๐Ÿ˜•) that variable will not be provided (or will, but incorrectly) from outside
  3. This error still happens in SSR mode. Vite creates commonjs bundles, so when someone is working with HMR using vite devServer in SSR mode, your package will be bundled and __DEV__ condition will look like this
    ssr-bug

Steps to reproduce the SSR bug:

  1. clone https://github.com/levchak0910/vue3-ssr-realworld-example-app
  2. checkout to branch: harlem-bug
  3. yarn dev
  4. update something in file /src/store/user.ts
  5. see bug

Or one another is here

DX:

  1. People, who see this warning and don't know the project's codebase, will not understand what is going on and why they see that warning, meanwhile they didn't write anything irrelevant
  2. People won't understand whether they wrote somewhere really duplicate store or it is just HMR
  3. Inconsistent behavior for dev and prod env. People write the code and don't see (or don't understand) this warning and everything works OK. But when they push code to server, create a build, and deliver the code to users, there will be thrown a real error instead replacing the store

Possible solution

On my opinion handling vite's specific bundling process should not be a part of this library, but instead could be handled by a plugin

I would suggest to do 2 things:

  1. Create new event (by eventEmitter) called store:beforeCreate (or whatever) and dispatch that before creating a store (obviously ๐Ÿ˜„), which will provide name of creating store and writable stores map
  2. Create new plugin called harlem-plugin-vite. In this plugin we will listen event store:beforeCreate and delete creating store from writable stores map before creating, so function createStore will be able to work as intended

By this solution we will have:

  1. consistant behavior in dev and prod env
  2. no breaking changes
  3. no confusing warnings

What do you think about all of it?
If you agree I can write all this staff and make pull request

Restore Harlem State from Storage

Describe the bug
When using the storage extension the state of the store is correctly saved in to the localStorage and shared between tabs but when reloading the page the state is not read from the storage.

I looked into the code and it looks like that is not something that was programmed in, but I feel like it could be part of the extension and quite easy to integrate.

To Reproduce
Steps to reproduce the behaviour:

  1. Create a store with the storage extension
  2. Change the state
  3. Reload the website

Expected behaviour
Restore the state of the store after a reaload

Version (please complete the following information):

  • Harlem: 2.2.0
  • Vue: 3.2.24
  • Runtime Environment: Any browser

Loss of prototype when cloning payload in mutations

HI!

function cloneObject(input: Record<PropertyKey, unknown>): Record<PropertyKey, unknown> {
const output: Record<PropertyKey, unknown> = {};
for (const key in input) {
output[key] = clone(input[key]);
}
return output;
}

In my opinion, copying objects in this way is not quite right. The fact is that this method of cloning leads to the loss of the prototype.

class OrderItem {
    amount: number;
    id: number;

    item__name: string;
    options: [];

    @Transform(({ value }) => Money.of(value))
    price: Money;

    priceByQuantity(): Money {
        return Money.multiply(this.price, this.amount);
    }
}

setItemMutation(plainToClass(OrderItem, someRawData))

store.item.priceByQuantity() // there will be a runtime error

I solved the problem as follows:

import cloneDeep from 'lodash/cloneDeep';

export const { state, getter, mutation, action, ...store } = createStore(NAME, STATE, {
    extensions: [actionExtension()],
    providers: {
        read: (state) => state,
        write: (state) => state,
        payload: (payload) => cloneDeep(payload),
    },
});

Thanks!

Error [ERR_REQUIRE_ESM]: Must use import to load ES Module

Describe the bug
I have a simple template of Vue application build with Vite. After update to Harlem 2.0.0-beta.0 I have following error:

20:14:50 [vite] Error when evaluating SSR module /src/entry-server.ts:
Error [ERR_REQUIRE_ESM]: Must use import to load ES Module: C:\Projects\vite-template\node_modules\.pnpm\@[email protected]\node_modules\@harlem\utilities\dist\index.js
require() of ES modules is not supported.
require() of C:\Projects\vite-template\node_modules\.pnpm\@[email protected]\node_modules\@harlem\utilities\dist\index.js from C:\Projects\vite-template\node_modules\.pnpm\@[email protected][email protected]\node_modules\@harlem\core\dist\index.cjs is an ES module file as it is a .js file whose nearest parent package.json contains "type": "module" which defines all .js files in that package scope as ES modules.
Instead rename index.js to end in .cjs, change the requiring code to use import(), or remove "type": "module" from C:\Projects\vite-template\node_modules\.pnpm\@[email protected]\node_modules\@harlem\utilities\package.json.

    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1080:13)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)
    at Module.require (internal/modules/cjs/loader.js:952:19)
    at n (C:\Projects\vite-template\node_modules\.pnpm\[email protected]\node_modules\jiti\dist\v8cache.js:2:2472)
    at Object.<anonymous> (C:\Projects\vite-template\node_modules\.pnpm\@[email protected][email protected]\node_modules\@harlem\core\dist\index.cjs:80:18)
    at Module.o._compile (C:\Projects\vite-template\node_modules\.pnpm\[email protected]\node_modules\jiti\dist\v8cache.js:2:2778)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1092:10)
    at Module.load (internal/modules/cjs/loader.js:928:32)
    at Function.Module._load (internal/modules/cjs/loader.js:769:14)

Expected behaviour
Application should work correctly. Version 1.3.2 seems to work as expected.

Version (please complete the following information):

  • Harlem: 2.0.0-beta.0
  • Vue: 3.2.2
  • Vite: 2.5.0
  • Runtime Environment: Node.js 14.15.4

Documentation: Add comparison section

It would be great to see a section in the docs that highlights some of the important differences between this and other existing solutions.

I'm not sure why I would choose this over the official recommendations like Pinia or Vuex, etc.

I'm sure the value is there, but it's not clear.

_a.errors.clear is not a function

Describe the bug
When using the actions extension, calling any action throws the following error:

runtime-core.esm-bundler.js:38 [Vue warn]: Unhandled error during execution of native event handler 
  at <NavBar> 
  at <Index> 
  at <App key=1 > 
  at <NuxtRoot>
runtime-core.esm-bundler.js:218 Uncaught TypeError: _a.errors.clear is not a function
    at index.ts:114:98
    at Store.mutate (store.ts:204:22)
    at mutation (store.ts:229:37)
    at Store.write (store.ts:231:60)
    at clearErrors (index.ts:114:20)
    at Proxy.<anonymous> (index.ts:146:21)
    at _createElementVNode.onClick._cache.<computed>._cache.<computed> (NavBar.vue:118:42)
    at callWithErrorHandling (runtime-core.esm-bundler.js:155:22)
    at callWithAsyncErrorHandling (runtime-core.esm-bundler.js:164:21)
    at HTMLButtonElement.invoker (runtime-dom.esm-bundler.js:369:13)

To Reproduce
https://github.com/florian-lefebvre/nuxt3-starter-medusa/tree/04f7fafa1afd40259b0af303afe2d8e6ad2ef226

Files to look at are /stores/useStore.ts (and the init action) and /components/Layout/NavBar.vue.

Steps to reproduce the behaviour:

  1. Install the dependencies (yarn)
  2. Start the Nuxt3 app (yarn dev)
  3. Click on INIT at the top

Expected behaviour
Nothing should happen, no errors.

Version (please complete the following information):

  • Harlem: 2.3.3
  • Nuxt: 3.0.0-rc.1
  • Runtime Environment: Brave V1.38.109 on Windows 10

Storage: cookie support

Is your feature request related to a problem? Please describe.

When doing SSR apps (with Nuxt 3 for instance), storing data is useful to it on both server and client side.

Describe the solution you'd like

I'd like to be able to choose cookie as the storage type. BTW, as Nuxt 3 provides a useCookie composable, it might be interesting to import the package using this composable like @harlem/extension-storage/nuxt (or this might concern the official nuxt module).

Describe alternatives you've considered

None for now as I'm considering switching from pinia.

Additional context

None.

Duplicate initialization on HMR with vite

Describe the bug
i'm using vue with typescript and vite. the problem is everytime i'm updating a file where i call a mutation in that component
the HMR always failed to reload because harlem keep trying to create a new store

My Component

<script lang="ts">
import {
computed,
ComputedRef,
defineComponent,
inject,
onMounted,
PropType,
} from 'vue';
import { getError, getLoadingButton } from '@/store';
import { useFile } from '@/components/hooks/useFile';
import { decreaseTimer, getTimer, getUser, setTimer } from '@/store/user';

export default defineComponent({
name: 'ProfileForm',
props: {
	user: {
		type: Object as PropType<User>,
		required: true,
	},
},
setup: function () {
	const { selectedFile, urlPreview, onChangeFile } = useFile();
	const editProfile = inject<(file: File) => void>('editProfile') as (
		file: File,
	) => void;
	const resendEmail = inject<() => void>(
		'resendEmailVerification',
	) as () => void;
	const openDialog = inject<() => void>('openDialog') as () => void;

	const avatarUrl = computed<string | undefined>(() => {
		const user = getUser.value;
		if (!user?.avatar_thumbnail && !urlPreview.value) {
			return '/assets/images/input-avatar.png';
		} else if (urlPreview.value) {
			return urlPreview.value;
		} else {
			return user?.avatar_thumbnail;
		}
	});
	let timer: number;
	const timerFn = () => {
		timer = setInterval(() => {
			if (getTimer.value.time <= 0) {
				clearInterval(timer);
				setTimer({ isCountdown: false, time: 60 });
			} else {
				decreaseTimer();
				setTimer({
					isCountdown: true,
					time: getTimer.value.time,
				});
			}
		}, 1000);
	};
	const resendEmailVerification = () => {
		timerFn();
		resendEmail();
	};
	onMounted(() => {
		if (getTimer.value.isCountdown) timerFn();
	});
	return {
		onChangeFile,
		selectedFile,
		avatarUrl,
		editProfile,
		openDialog,
		resendEmailVerification,
		timer: getTimer as ComputedRef<{
			isCountdown: boolean;
			time: number;
		}>,
		getUser,
		loading: getLoadingButton,
		errors: getError,
	};
},
});
</script>

My User Store

import { createStore } from '@harlem/core';
import { ComputedRef } from 'vue';
import { RouteStatus } from '@/routes';

interface State {
	user: User | null;
	status: RouteStatus.USER | RouteStatus.SELLER;
	timer: {
		isCountdown: boolean;
		time: number;
	};
}

const STATE = <State>{
	user: null,
	status: RouteStatus.USER,
	timer: {
		isCountdown: false,
		time: 60,
	},
};

const { getter, mutation } = createStore('user', STATE);

export const setUser = mutation<User>('setUser', (state, payload) => {
	state.user = payload;
	localStorage.setItem('user', JSON.stringify(payload));
});

export const removeUser = mutation('removeUser', state => {
	localStorage.removeItem('user');
	state.user = null;
});

export const getUser = getter('getUser', state => {
	const localUser: string | null = localStorage.getItem('user');
	if (localUser) {
		setUser(JSON.parse(localUser) as User);
	}
	return state.user;
});

export const isAuthorized = getter('isAuthorized', () =>
	checkAuthorization(getUser),
);

export const checkAuthorization = (
	user: ComputedRef<User | null>,
): user is ComputedRef<User> => {
	return user.value !== null;
};

export const setSeller = mutation<boolean>('setSeller', (state, payload) => {
	if (payload) state.status = RouteStatus.SELLER;
	else state.status = RouteStatus.USER;
});

export const isSeller = getter('isSeller', state => {
	return state.status === RouteStatus.SELLER;
});
export const getStatus = getter('getStatus', state => {
	return state.status;
});

export const getToken = getter('getToken', state => {
	return state.user?.token;
});

export const setTimer = mutation<{ isCountdown: boolean; time: number }>(
	'setTimer',
	(state, payload) => {
		localStorage.setItem('timer', JSON.stringify(payload));
		state.timer = payload;
	},
);

export const decreaseTimer = mutation('decreaseTimer', state => {
	state.timer.time--;
});
export const getTimer = getter('getTimer', state => {
	let localTimer: { isCountdown: boolean; time: number } | null = JSON.parse(
		<string>localStorage.getItem('timer'),
	);
	if (localTimer) {
		setTimer(localTimer);
	}
	return state.timer;
});

To Reproduce

Steps to reproduce the behaviour:

  1. install vue with vite
  2. create a store
  3. start the server
  4. call a mutation from component
  5. do some change on the component
  6. See error when the hmr try to reloading the page

Expected behaviour
Supposedly the store need to reset the instance and create a new one when hmr reload

Screenshots
image

Version (please complete the following information):

  • Harlem: [e.g. 1.1.1]
  • Vue: [e.g. 3.0.5]
  • Runtime Environment: Version 89.0.4389.90 (Official Build) (Windows 10 64-bit)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.