npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

treeduxjs

v1.0.0

Published

Lightweight wrapper for Redux providing fully type-hinted state tree traversal out of the box

Downloads

28

Readme

Treedux

Treedux is a lightweight wrapper for Redux providing fully type-hinted state tree traversal out of the box.

Features:

  • Uses TypeScript generics to provide easy traversal of the full state tree out of the box
  • Default methods available on every tree node to get, set and subscribe to changes without writing any boilerplate code or reducers
  • Ability to override any node in the state tree to add custom reducers and action creators (known as Mutators)
  • Full support for React hooks to further reduce boilerplate code when used with functional components

Rationale

To modify the application state, Redux requires you to write a reducer to perform the update. The vast majority of the reducers we write are incredibly simple and do nothing more than directly set the value of a particular property with a new value. This logic is often duplicated for most of the properties in each data store, with the only difference being the name of the property it's responsible for updating. This leads to a lot of duplicated boilerplate code to perform a series of very simple operations.

Treedux is designed to be the antidote to this. Out of the box, it allows you to traverse through your application state tree (fully type-hinted using TypeScript generics) and at each tree node get the current value, update the value with a new one or subscribe to changes, all without writing a single reducer. This allows you to get up and running super quickly with minimal code required.

Installation

Using npm:

npm install treeduxjs

Using yarn:

yarn add treeduxjs

Example Usage

1. Creating a Data Store

To get started, you'll need to create one or more data stores. Each data store requires a unique key/name, an interface or type describing the shape of the data store's state and the initial state of the data store.

// UserStore.ts

import { DataStore } from 'treedux';

export enum UserPreferenceEnum {
  DARK_MODE = "dark_mode",
  SHOW_NOTIFICATIONS = "show_notifications"
}

export interface UserStoreInterface
{
  user: {
    name: string,
    age: number
  },
  preferences: Array<UserPreferenceEnum>
}

export class UserStore
{
  public static KEY: "user" = "user";
  
  public static create()
  {
    return DataStore.create<UserStoreInterface>(
      UserStore.KEY,
      {
        initialState: {
          user: {
            name: "John McClane",
            age:  32
          },
          preferences: [
            UserPreferenceEnum.SHOW_NOTIFICATIONS
          ]
        }
      }
    );
  }
}

2. Initialising Treedux

// index.ts

import { Treedux } from "treeduxjs";
import { UserStore } from "./UserStore";

const treedux = Treedux.init(
  // Data store map
  {
    [UserStore.KEY]: UserStore.create()
  },
  // Options
  {
    // initialState: { ... } // You can optionally pass in the initial state of your application here
  }
);

3. Using Default Methods

You can now use the state property on the Treedux instance to traverse the state tree. Out of the box, each node on the tree provides methods to get, set and subscribe to changes.

const userNode = treedux.state.user.user;

// Get the current value
const value = userNode.get();

console.log('Initial value of user', value); // { name: "John McClane", age: 32 }

// Subscribe to changes
const unsubscribe = userNode.subscribe((updatedUser) => {
  console.log('User updated', updatedUser);
});

// Stop listening for changes by calling the unsubscribe function
// unsubscribe();

// Update the name with a new value
userNode
  .set({ name: 'Holly Gennero', age:  30 }) // The set method returns an action (calling set alone will not dispatch the action)
  .dispatch(); // The dispatch method will actually dispatch the action to the store and update the state

4. Using Dynamic Nodes

For the keys that are explicitly specified in your data store's interface, you can use the type-hinted properties to traverse the tree. However, sometimes parts of the state tree use index signatures or other dynamic keys that can't be explicitly type-hinted. In these cases, you can use the byKey and delete methods to traverse the tree and delete dynamically created data. These additional methods are only available on nodes that are dynamic and can't be type-hinted in the usual way.

Let's take the following data store interface as an example. It tracks the number of ads and trackers blocked on each domain for each tab in the browser.


interface AdblockStats
{
  stats: {
    [tabId: number]: {
      [domain: string]: {
        adsBlocked: number,
        trackersBlocked: number
      }
    }
  }
}

As the tabs and domains are both created dynamically and can't be explicitly typed, we'll use the byKey method to traverse the tree.

// All the usual methods are available as they would be on any other state node
const stateNode = treedux.state.adblock.stats.byKey(123).byKey('example.com');

// We can get the current value
const currentValue = stateNode.get();

// We can subscribe to changes
stateNode.subscribe((stats) => {
  console.log('Stats updated', stats);
})

// And we can set a new value
stateNode.set({
  adsBlocked: 123,
  trackersBlocked: 456
});

These dynamic nodes also have access to the delete method which completely removes the data from the state tree (rather than setting its value to null or undefined).

// Delete the stat for the domain 'example.com' on tabId 123
treedux.state.adblock.stats.byKey(123).byKey('example.com').delete().dispatch();
// Delete all stats for the tab with an id of 123
treedux.state.adblock.stats.byKey(123).delete().dispatch();

5. Using Mutators

Sometimes, you need to perform more complex updates to the state and the default set method isn't enough. This is especially true for data structures like arrays where getting the current value, pushing an item in then setting it again could introduce race conditions.

In these cases, you can override any node in the state tree and add custom reducers and action creators (known as Mutators). This is done by passing a mutator map to the create method on the DataStore class.

Let's create a mutator to add to the user preferences array. The mutator should extend the AbstractMutator class and implement the getType, getAction and reduce methods.

The getType method must return a unique string. This is used to identify the mutator and find its reducer when the action is dispatched.

The getAction method should return an instance of the Action class. The type of the action should match the value returned by the getType method.

The reduce method contains the logic that performs the update to the state. The first parameter is the current state of the data store and the second is the action that is being dispatched.

// AddPreferenceMutator.ts

import { AbstractMutator, Action } from "treeduxjs";
import { UserStoreInterface } from "./UserStore";

class AddPreferenceMutator extends AbstractMutator<UserStoreInterface>
{
  public getType(): string
  {
    return "user/add_preference";
  }
  
  public getAction(...preferences: Array<UserPreferenceEnum>): Action<Array<UserPreferenceEnum>>
  {
    return Action.create(
      {
        type: this.getType(), 
        payload: preferences
      },
      this.treedux
    );
  }
  
  public reduce(state: UserStoreInterface, action: { type: string, payload: Array<UserPreferenceEnum> }): void
  {
    state.preferences.push(
      ...action.payload.filter((preference) => !state.preferences.includes(preference))
    );
  }
}

Now we can update our UserStore to register the mutator when the store is created.

// UserStore.ts

import { DataStore } from 'treedux';
import { AddPreferenceMutator } from './AddPreferenceMutator';

export enum UserPreferenceEnum {
  DARK_MODE = "dark_mode",
  SHOW_NOTIFICATIONS = "show_notifications"
}

export interface UserStoreInterface
{
  user: {
    name: string,
    age: number
  },
  preferences: Array<UserPreferenceEnum>
}

export class UserStore
{
  public static KEY: "user" = "user";
  // The mutators object must mirror the structure of your data store and each node accepts an object 
  // where the key is the method name and the value is a function that takes a Treedux instance as 
  // an argument and returns an instance of the mutator
  private static readonly mutators = {
    preferences: {
      add: (treedux: Treedux) => new AddPreferenceMutator(treedux)
    },
  };
  
  public static create()
  {
    return DataStore.create<UserStoreInterface, typeof this.mutators>( // Notice the second generic parameter "typeof mutators" (this will be used to type-hint the mutators on the relevant node)
      UserStore.KEY,
      {
        initialState: {
          user: {
            name: "John McClane",
            age:  32
          },
          preferences: [
            UserPreferenceEnum.SHOW_NOTIFICATIONS
          ]
        },
        mutators: this.mutators // Your mutators must be passed to the DataStore.create method options under the "mutators" key
      }
    );
  }
}

Now we can use the mutator to add a new preference to the array.


treedux
  .state
  .user
  .preferences
  .add(UserPreferenceEnum.DARK_MODE) // The add method is now type-hinted for the getAction() method on the mutator
  .dispatch();

6. Using React Hooks

Each node on the state tree also provides a React hook through the use method that exposes the current value, the set method and any mutator methods like the add method in the previous example. The hook will automatically unsubscribe when the component unmounts.

function ExampleComponent()
{
  const { value: user, set: setUser } = treedux.state.user.user.use();
  const { value: preferences, add: addPreference } = treedux.state.user.preferences.use();
  
  
  return <div>
    <h5>Name: {user?.name}</h5>
    
    <h6>Preferences:</h6>
    
    <ul>
      {preferences.map((preference, index) => <li key={index}>{preference}</li>)}
    </ul>
    
    <button
      onClick={() => addPreference(UserPreferenceEnum.DARK_MODE).dispatch()}
    >
      Enable Dark Mode
    </button>
  </div>;
}

In order for the use method to work properly, you'll need to give Treedux the useState and useEffect hooks from the version of React you're using through the options object when initialising Treedux:

// index.ts

import { Treedux } from "treeduxjs";
import { UserStore } from "./UserStore";
import { useState, useEffect } from "react";

const treedux = Treedux.init(
  // Data store map
  {
    [UserStore.KEY]: UserStore.create()
  },
  // Options
  {
    // initialState: { ... } // You can optionally pass in the initial state of your application here
    hooks: {
      useState:  useState,
      useEffect: useEffect
    }
  }
);