0.1.2 • Published 6 months ago

modelez v0.1.2

Weekly downloads
-
License
-
Repository
-
Last release
6 months ago

Modelez

Modelez is a lightweight and powerful reactive state management library for Vanilla JavaScript and React. It provides an elegant and efficient way to manage application state, logic, and asynchronous resources. Modelez supports dynamic reactivity, conditional reactive states, multiple storages, and seamless integration with React.


Features

  • 🔄 Reactive State: Define and subscribe to reactive states with ease.
  • Async Resource Management: Handle async logic with built-in support for promises and Suspense.
  • 🔃 Conditional Reactivity: Fine-tuned control over reactive updates.
  • 🛠️ Multiple Storages: Manage state across multiple storage backends.
  • 🧩 VanillaJS and React Support: Use Modelez with plain JavaScript or integrate seamlessly into React.

Installation

Install Modelez via npm:

npm install modelez

via yarn:

yarn add modelez

Basic Usage

Vanilla JavaScript

Modelez provides reactive state management for Vanilla JavaScript applications.

import { model, container, effect } from "modelez";

// Set up the HTML structure
document.body.innerHTML = `<button id="clickMe"></button>`;

// Create a container to manage all models
const app = container();

// Define a Counter model
const CounterModel = model("counter", ({ state }) => {
  const count = state(0);

  return {
    count,
    increment() {
      count((prev) => prev + 1);
    },
  };
});

const counter = app.get(CounterModel);
const button = document.getElementById("clickMe");

// Create an effect to reactively update the button text
effect({ count: counter.count }, (deps) => {
  button.textContent = `Count: ${deps.count}`;
});

// increase counter when button clicked
button.addEventListener("click", () => {
  counter.increment();
});

React Integration

Modelez integrates seamlessly with React through hooks and providers.

import { useReactive, Provider } from "modelez/react";
import { app } from "./app";
import { CounterModel } from "./counterModel";

// Counter component
export const Counter = () => {
  const { count, increment } = useReactive(CounterModel);

  return <button onClick={increment}>Count: {count}</button>;
};

export const App = () => {
  return (
    <Provider container={app}>
      <Counter />
    </Provider>
  );
};

Advanced Usage

Using Multiple Storages

import { container, model, createInMemoryStorage } from "modelez";

const getUrlParams = () => new URLSearchParams(window.location.search);

// create URL state storage
const urlStateStorage = {
  get(key) {
    const urlParams = getUrlParams();
    return urlParams.get(key);
  },
  set(key, value) {
    const urlParams = getUrlParams();
    urlParams.set(key, value);
    // Update the URL without reloading the page
    const newUrl = `${window.location.pathname}?${urlParams.toString()}`;
    window.history.pushState({}, "", newUrl);
  },
  has(key) {
    return getUrlParams().has(key);
  },
};

// Define storages
const inMemoryStorage = createInMemoryStorage();
const storages = {
  default: inMemoryStorage, // Default storage
  url: urlStateStorage, // URL-based storage
};

// Create a container with multiple storages
const app = container({ storages });

// Define a model with states stored in different storages
const AppModel = model("app", ({ state }) => {
  // State stored in default (in-memory) storage
  const localState = state("localCount", 0);

  // State stored in URL storage
  const urlState = state("url:page", "home");

  return {
    localState,
    urlState,
    incrementLocal() {
      localState((prev) => prev + 1);
    },
    setPage(page) {
      urlState(page);
    },
  };
});

// Access and use the model
const appModel = app.get(AppModel);

// Update and log the states
appModel.incrementLocal();
console.log("Local State:", appModel.localState());

appModel.setPage("about");
console.log("URL State (Page):", appModel.urlState());

Async Resource Management

Modelez makes it simple to handle asynchronous data fetching.

Defining a Resource

import { model } from "modelez";

const BlogListModel = model("blogList", ({ resource }) => {
  const posts = resource(() => fetch("/api/posts").then((res) => res.json()));

  return {
    posts,
    async reload() {
      await posts.reload();
    },
  };
});

Using the Resource in React

import { useReactive } from "modelez/react";

const BlogListWithLoadingIndicator = () => {
  const { posts } = useReactive(BlogListModel);

  if (posts.loading) {
    return <LoadingIndicator />;
  }

  if (posts.error) {
    return <Error error={posts.error} />;
  }

  return (
    <ul>
      {posts.value.map((post) => (
        <BlogPost key={post.id} post={post} />
      ))}
    </ul>
  );
};

Promise-Like Resources Outside React

The resource object implements the PromiseLike interface, allowing you to use .then() for direct access.

const blogList = app.get(BlogListModel);

blogList.posts.then((posts) => {
  console.log(posts);
});

Using Suspense for Async Resources

import { Suspense } from "react";
import { useReactive } from "modelez/react";

const BlogList = () => {
  const { posts } = useReactive(BlogListModel);

  return (
    <ul>
      {posts.value.map((post) => (
        <BlogPost key={post.id} post={post} />
      ))}
    </ul>
  );
};

const BlogListWithSuspense = () => {
  return (
    <ErrorBoundary>
      <Suspense fallback={<LoadingIndicator />}>
        <BlogList />
      </Suspense>
    </ErrorBoundary>
  );
};

Dependency Injection with Container

import { container } from "modelez";
import { useExternalHook } from "./externalHook";
import { ExternalComponent } from "./externalComponent";

// Create a named container for dependency injection
export const dependencies = container().named({
  ExternalComponent,
  useExternalHook,
});

export const MyComp = () => {
  const result = dependencies.useExternalHook(); // Use the injected hook

  if (result) {
    return <dependencies.ExternalComponent />; // Render the injected component
  }

  return null;
};

Unit Testing Example

import { dependencies, MyComp } from "./myComp";
import { render } from "@testing-library/react";

describe("MyComp", () => {
  let restore: VoidFunction;

  beforeEach(() => {
    // Backup the current dependencies before mocking
    restore = dependencies.$container.backup();
  });

  afterEach(() => {
    // Restore the original dependencies after each test
    restore?.();
  });

  it("should render ExternalComponent if result = true", () => {
    // Mocking useExternalHook to return true
    dependencies.useExternalHook = () => true;

    // Mocking ExternalComponent to render a simple div
    dependencies.ExternalComponent = () => <div>ok</div>;

    // Render the component
    const { getByText } = render(<MyComp />);

    // Verify that "ok" text is displayed
    getByText("ok");
  });

  it("should not render ExternalComponent if result = false", () => {
    // Mocking useExternalHook to return false
    dependencies.useExternalHook = () => false;

    // Render the component
    const { queryByText } = render(<MyComp />);

    // Verify that "ok" text is not displayed
    expect(queryByText("ok")).toBeNull();
  });
});

API Reference

model(name: string, factory: Function)

Defines a model with reactive states and logic.

  • name: A unique identifier for the model.
  • factory: A function returning the model logic and reactive states.

container()

Creates a container to manage and resolve models.

useReactive(model: Model)

A React hook to use a model's reactive states and methods.

effect(dependencies: object, callback: Function)

Tracks dependencies and re-runs the callback when they change.

resource(initializer: Function)

Creates a reactive async resource.

0.1.2

6 months ago

0.1.1

6 months ago

0.1.0

6 months ago