4.0.1 • Published 9 months ago

@ibnlanre/portal v4.0.1

Weekly downloads
-
License
BSD-3-Clause
Repository
github
Last release
9 months ago

@ibnlanre/portal

A TypeScript state management library for React applications.

Table of Contents

Installation

To install @ibnlanre/portal, you can use either a CDN, or a package manager. Run the following command within your project directory. The library is written in TypeScript and ships with its own type definitions, so you don't need to install any type definitions.

Using Package Managers

If you are working on a project that uses a package manager, you can copy one of the following commands to install @ibnlanre/portal using your preferred package manager, whether it's npm, pnpm, or yarn.

npm

npm i @ibnlanre/portal

pnpm

pnpm add @ibnlanre/portal

yarn

yarn add @ibnlanre/portal

Using a CDN

If you are working on a project that uses markup languages like HTML or XML, you can copy one of the following script tags to install @ibnlanre/portal using your preferred CDN:

skypack

<script type="module">
  import { createStore } from "https://cdn.skypack.dev/@ibnlanre/portal";
</script>

unpkg

<script src="https://unpkg.com/@ibnlanre/portal"></script>

jsDelivr

<script src="https://cdn.jsdelivr.net/npm/@ibnlanre/portal"></script>

Usage

@ibnlanre/portal helps you manage state using a simple and intuitive API. Its aim is to provide a flexible and scalable solution for managing state in your React applications. The library is designed to be easy to use and integrate with existing codebases. Managing application state should not be a complex task, and @ibnlanre/portal aims to simplify the process.

Managing State

State management using this library is as simple as calling the createStore function with an initial value. The function returns an object with methods to access and update the state. These methods are $get, $set, and $use. The $get method returns the current state, the $set method updates the state, and the $use method is a React hook that allows you to manage the state within functional components.

import { createStore } from "@ibnlanre/portal";

const store = createStore("initial value");

$get Method

The $get method is a synchronous operation that returns the current state. It is useful when you want to access the state without subscribing to state changes.

const value = store.$get();
console.log(value); // "initial value"

$set Method

The $set method is used to update the state. It takes a new value as an argument and updates the state with that value. It can also take a callback function that is called with the previous value. This allows you to update the state based on the previous value. Note that the $set method updates the state immediately.

store.$set("new value");
const newValue = store.$get(); // "new value"

$sub Method

Asides the aforementioned methods, the store object also has a $sub method to subscribe to state changes. This method takes a callback function that is called whenever the state changes. This allows you to react to state changes and perform side effects based on the new state.

store.$sub((value) => console.log(value));

The return value of the $sub method is a function that can be called to unsubscribe from the store. This is useful when you want to stop listening to state changes, such as when a component is unmounted.

Note that the callback function is called immediately after subscribing to the store. To opt out of this behavior, you can pass false as the second argument to the $sub method. This prevents the callback function from being called immediately.

const unsubscribe = store.$sub((value) => {
  console.log(value);
}, false);

unsubscribe();

Chained Store

Asides regular stores, you can also create chained stores by passing an object to the createStore function. The object can contain nested objects, arrays, and primitive values. Each store in the chain has access to its own value, with methods to update and manage that value. This allows you to work with state at any level of the object hierarchy, with minimal effort.

const store = createStore({
  location: {
    unit: "Apt 1",
    address: {
      street: "123 Main St",
      city: "Springfield",
    },
  },
});

const city = store.location.address.city.$get();
console.log(city); // "Springfield"

Breaking Off Stores

Remember that each point in an object chain is a store, and can be broken off into its own store at any point in the chain. This is useful when you want to work with a nested state independently of the parent store. For example, you can break off the address store from the location store and work with it independently.

const { address } = store.location;

const street = address.street.$get();
console.log(street); // "123 Main St"

Updating Nested Stores

Updating a nested store works similarly to updating a regular store. You can use the $set method to change the value of a nested store. Additionally, you can pass a callback function to the $set method to modify the value based on the previous state.

Note that since a nested store shares the same state across all levels of the object hierarchy, updating a nested store will also update the parent store. This behaviour is intentional, and allows you to work with state at any level of the object hierarchy, as if it were a regular store.

const { street } = store.location.address;

street.$set("456 Elm St");
street.$set((prev) => `${prev} Apt 2`);

Asynchronous State

The createStore function can also accept an asynchronous function that returns a promise. This is useful for fetching data asynchronously. The function must return a promise that resolves to the initial state.

Note that the store will be empty until the promise resolves. Additionally, even if the promise returns an object, it would be treated as a primitive value. This is because chained stores are created from objects passed to the createStore function, during initialization.

type State = { apartment: string };

async function fetchData(): Promise<State> {
  const response = await fetch("https://api.example.com/data");
  return response.json();
}

const store = await createStore(fetchData);
const state = store.$get(); // { apartment: "123 Main St" }

React Integration

@ibnlanre/portal has first-class support for React applications. When a store is created, it creates a React hook, $use, that allows you to manage the store's state within functional components. Just like the useState hook in React, the $use hook returns an array with two elements: the state value and a dispatch function to update the state.

The benefit of using the $use hook over $get and $set is that it automatically subscribes to state changes and updates the component when the state changes. This allows you to manage the state within the component, without having to manually subscribe to state changes. Additionally, the $use hook automatically unsubscribes from the store when the component is unmounted.

import type { ChangeEvent } from "react";
import { store } from "./path-to-your-store";

function Component() {
  const [state, setState] = store.$use();

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    return setState(event.target.value);
  };

  return <input value={value} onChange={handleChange} />;
}

export default Component;

Persistence

@ibnlanre/portal provides storage adapters for local storage, session storage, and cookies. These adapters allow you to persist state across sessions and devices. The storage adapters are created using the createLocalStorageAdapter, createSessionStorageAdapter, and createCookieStorageAdapter functions.

Each function takes a key as a parameter and returns two functions: one to get the state and the other to set the state. The createStore function can then be used with these storage adapters to create a store that persists state in the web storage.

Web Storage Adapter

To persist state within the local/session storage of the browser, you can use either the createLocalStorageAdapter or createSessionStorageAdapter functions respectively. The parameters for these functions are the same. They are as follows:

  • key: The key used to store the state in the web storage.
  • stringify: A function to serialize the state to a string. The default is JSON.stringify.
  • parse: A function to deserialize the state from a string. The default is JSON.parse.

Local Storage Adapter

The only required parameter for the web storage adapters is the key. This is the key used to store the state in the storage, as well as, to retrieve and update the state from the storage. It's important to note that the key is unique to each store, and should be unique to prevent conflicts with other stores.

import { createLocalStorageAdapter } from "@ibnlanre/portal";

const [getLocalStorageState, setLocalStorageState] = createLocalStorageAdapter({
  key: "storage",
});

Through the stringify and parse functions, you can customize how the state is serialized and deserialized. This is useful when working with non-JSON serializable values or when you want to encrypt the state before storing it in the web storage.

import { createLocalStorageAdapter } from "@ibnlanre/portal";

const [getLocalStorageState, setLocalStorageState] = createLocalStorageAdapter({
  key: "storage",
  stringify: (state) => btoa(state),
  parse: (state) => atob(state),
});

Session Storage Adapter

The benefit of using the storage adapters is that they automatically load the state from the storage when the store is created. This allows you to initialize the store with the persisted state. Additionally, the storage adapters provide a way to update the storage when the store changes. This is done by subscribing to the store changes and updating the storage with the new state.

import { createStore } from "@ibnlanre/portal";

const store = createStore(getSessionStorageState);
store.$sub(setSessionStorageState);

In situations where the store requires an initial value of some sort, you can pass the fallback state to the getLocalStorageState or getSessionStorageState functions. This allows you to initialize the store with the fallback state if the state is not found in the storage.

import { createStore, getSessionStorageState } from "@ibnlanre/portal";

const fallbackState = "initial value";
const store = createStore(() => getSessionStorageState(fallbackState));

Browser Storage Adapter

Although the storage adapters provided by the library are a select few, if you need to use a different storage mechanism, such as IndexedDB or a custom API, you can also create your own custom browser storage adapter. The browser storage adapter only requires a key, and functions to get the item, set the item, and remove the item from the storage. Other options like stringify and parse are optional.

import { createStore, createBrowserStorageAdapter } from "@ibnlanre/portal";

const storage = new Storage();

const [getStorageState, setStorageState] = createBrowserStorageAdapter({
  key: "storage",
  getItem: (key: string) => storage.getItem(key),
  setItem: (key: string, value: string) => storage.setItem(key, value),
  removeItem: (key: string) => storage.removeItem(key),
});

Cookie Storage Adapter

The last of the storage adapters provided by the library is the cookie storage adapter. It is created using the createCookieStorageAdapter function, which takes a key and an optional configuration object with cookie options. These options are similar to those provided by the document.cookie API.

import { createCookieStorageAdapter } from "@ibnlanre/portal";

const [getCookieStorageState, setCookieStorageState] =
  createCookieStorageAdapter({
    key: "storage",
    domain: "example.com",
    maxAge: 60 * 60 * 24 * 7, // 1 week
    path: "/",
  });

Signed Cookies

To enhance security, you can provide a secret parameter to sign the cookie value. This adds an extra layer of protection, ensuring the cookie value has not been tampered with. To use the secret value, set signed to true.

Note that an error will be thrown if signed is set to true and the secret is not provided. To prevent this, ensure that the secret is provided or set signed to false to disable signing.

import { createCookieStorageAdapter } from "@ibnlanre/portal";

const secret = process.env.COOKIE_SECRET_KEY;

if (!secret) {
  throw new Error("Cookie secret key is required");
}

const [getCookieStorageState, setCookieStorageState] =
  createCookieStorageAdapter({
    key: "storage",
    sameSite: "Strict",
    signed: !!secret,
    secret,
  });

Updating Cookie Options

One key difference between the createCookieStorageAdapter function and the other storage adapter functions is that its setCookieStorageState function takes an additional parameter: the cookie options. This allows you to update the initial cookie options when setting the cookie value. This is useful when you want to update the max-age or expires options of the cookie.

const store = createStore(getCookieStorageState);

store.$sub((value) => {
  const expires = new Date(Date.now() + 15 * 60 * 1000);

  setCookieStorageState(value, {
    secure: true,
    partitioned: false,
    httpOnly: true,
    expires,
  });
});

Cookie Storage

One key module provided by the library is the cookieStorage module. This module provides a set of functions to manage cookies in a web application. Just like localStorage and sessionStorage, cookies are a way to store data in the browser.

Motivation

Cookies are useful for storing small amounts of data that need to be sent with each request to the server. However, most modern web browsers do not have a means to manage cookies, and this is where the cookieStorage module comes in. Accessing the cookieStorage module is similar to accessing the other modules provided by the library.

import { cookieStorage } from "@ibnlanre/portal";

Utility Functions

All functions in the cookieStorage module are static and do not require an instance of the module to be created. This is because the module is a utility module that provides a set of functions to manage cookies. The following are the functions provided by the cookieStorage module:

  • sign(value: string, secret: string): string

    • Signs a cookie value using a secret key.
    • Parameters:
      • value: The cookie value to be signed.
      • secret: The secret key used to sign the cookie.
    • Returns: The signed cookie value.
    const signedValue = cookieStorage.sign("value", "secret");
  • unsign(signedValue: string, secret: string): string

    • Unsigns a signed cookie value using a secret key.
    • Parameters:
      • signedValue: The signed cookie value to be unsigned.
      • secret: The secret key used to unsign the cookie.
    • Returns: The original cookie value.
    const originalValue = cookieStorage.unsign(signedValue, "secret");
  • getItem(key: string): string

    • Retrieves the value of a cookie by its key.
    • Parameters:
      • key: The key of the cookie to retrieve.
    • Returns: The value of the cookie.
    const value = cookieStorage.getItem("key");
  • setItem(key: string, value: string): void

    • Sets the value of a cookie.
    • Parameters:
      • key: The key of the cookie to set.
      • value: The value to set for the cookie.
    cookieStorage.setItem("key", "value");
  • removeItem(key: string): void

    • Removes a cookie by its key.
    • Parameters:
      • key: The key of the cookie to remove.
    cookieStorage.removeItem("key");
  • clear(): void

    • Clears all cookies.
    cookieStorage.clear();
  • key(index: number): string

    • Retrieves the key of a cookie by its index.
    • Parameters:
      • index: The index of the cookie to retrieve the key for.
    • Returns: The key of the cookie.
    const key = cookieStorage.key(0);
  • length: number

    • Retrieves the number of cookies stored.
    • Returns: The number of cookies.
    const length = cookieStorage.length;

Contributions

All contributions are welcome and appreciated. Thank you! 💚

License

This library is licensed under the BSD-3-Clause. See the LICENSE file for more information.

4.0.1

9 months ago

4.0.0

10 months ago

3.24.0-beta

2 years ago

3.23.3-beta

2 years ago

3.23.2-beta

2 years ago

3.23.1-beta

2 years ago

3.0.2-beta

2 years ago

3.3.1-beta

2 years ago

2.4.0

2 years ago

3.22.0-beta

2 years ago

3.11.0-beta

2 years ago

3.9.3-beta

2 years ago

3.9.0-beta

2 years ago

2.0.0

2 years ago

3.18.3-beta

2 years ago

3.6.0-beta

2 years ago

3.5.0-beta

2 years ago

3.2.0-beta

2 years ago

3.1.0-beta

2 years ago

0.7.2

2 years ago

0.7.1

2 years ago

0.7.4

2 years ago

0.7.3

2 years ago

3.22.1-beta

2 years ago

0.7.0

2 years ago

0.3.3

2 years ago

2.3.0

2 years ago

2.3.1

2 years ago

3.22.3-beta

2 years ago

0.8.0

2 years ago

0.4.1

2 years ago

0.4.0

2 years ago

0.4.2

2 years ago

1.0.2

2 years ago

1.0.1

2 years ago

1.0.0

2 years ago

3.18.1-beta

2 years ago

3.5.1-beta

2 years ago

2.2.0

2 years ago

3.22.2-beta

2 years ago

3.20.0-beta

2 years ago

3.7.0-beta

2 years ago

3.3.0-beta

2 years ago

3.4.0-beta

2 years ago

3.23.0-beta

2 years ago

3.0.0-beta

2 years ago

3.18.4-beta

2 years ago

0.5.0

2 years ago

0.5.2

2 years ago

0.5.1

2 years ago

3.20.1-beta

2 years ago

3.5.2-beta

2 years ago

0.7.6

2 years ago

0.7.5

2 years ago

0.7.8

2 years ago

0.7.7

2 years ago

3.19.1-beta

2 years ago

3.12.2-beta

2 years ago

3.9.2-beta

2 years ago

3.2.1-beta

2 years ago

3.0.1-beta

2 years ago

3.21.0-beta

2 years ago

3.10.0-beta

2 years ago

3.18.2-beta

2 years ago

2.1.0

2 years ago

3.8.0-beta

2 years ago

3.14.0-beta

2 years ago

3.12.0-beta

2 years ago

3.13.0-beta

2 years ago

3.19.0-beta

2 years ago

3.18.0-beta

2 years ago

3.15.0-beta

2 years ago

3.17.0-beta

2 years ago

3.16.0-beta

2 years ago

3.0.3-beta

2 years ago

3.9.1-beta

2 years ago

0.6.3

2 years ago

3.15.1-beta

2 years ago

3.13.1-beta

2 years ago

0.6.2

2 years ago

3.12.1-beta

2 years ago

0.6.4

2 years ago

3.3.2-beta

2 years ago

3.2.2-beta

2 years ago

0.6.1

2 years ago

3.14.1-beta

2 years ago

0.6.0

2 years ago

0.3.2

2 years ago

0.3.1

2 years ago

0.3.0

2 years ago

0.2.1

2 years ago

0.2.0

2 years ago

0.1.1

2 years ago

0.1.0

2 years ago

0.0.7

2 years ago

0.0.6

2 years ago

0.0.5

2 years ago

0.0.4

2 years ago

0.0.3

2 years ago

0.0.2

2 years ago

0.0.1

2 years ago