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

tenx

v2.1.13

Published

A Flux implementation for Javascript/React app

Downloads

8

Readme

tenx

A Flux implementation for javascript/React app

Installation

npm i tenx --save

Introduction

In this article, I am going to help you to learn the fundamentals of react and tenx. We will do this by implementing a todolist app using react and tenx.

Core concepts

Basically 2 core concepts in tenx:

  • Store
  • Actions

What is the store ?

The store is an object which holds all the app data. This holds the state of our app and this can be used everywhere in our project.

Unlikely redux, you can define many stores in your app, but we use only single store for todolist app

Actions

Actions are pure functions. You can only mutate app state inside the actions. So action is where you put all app logic.

An action takes two parameters. The first parameter is the store context. It contains many named state mutators and provides some util functions for advanced usages. The second parameter is an action payload.

Creating a simple todo app

Defining app state tree

Firstly, we need to define an app state tree. Todos state is an array of todo item, that looks like:

const state = {
  todos: [
    { id: 1, title: "Todo 1", completed: false },
    { id: 2, title: "Todo 2", completed: false },
  ],
};

Creating tenx store

We use tenx() function to create a store, the first parameter is initial state of store

import tenx from "tenx";
const initial = {
  // A store has one state, it names todos
  todos: [
    { id: 1, title: "Todo 1", completed: false },
    { id: 2, title: "Todo 2", completed: false },
  ],
};
const store = tenx(initial);

Defining actions

We pass action map as second parameter of tenx() function

const store = tenx(initial, {
  action1(context, payload) {},
  action2(context, payload) {},
});

Let's create a first addTodo action

const store = tenx(initial, {
  addTodo(context, payload) {
    // destructing context to retrieve todos state object
    const { todos } = context;
    // a payload is new todo title
    const title = payload;
    // retrieve current value of todos state object using todos.value
    // create a copy of todos array and append new item at end
    // assign back to todos state object using todos.value = newValue
    // DO NOT mutate state value directly: todos.value.push(newItem)
    todos.value = todos.value.concat({
      id: Date.now(),
      title,
      completed: false,
    });
  },
});

If you see the above code, we have created an action called ‘addTodo’. You can dispatch addTodo action by using store.actionName(payload):

// context object is passed automatically by store, we pass action payload only
store.addTodo("Todo 2");
store.addTodo("Todo 3");

Consuming store state

import React, { useRef } from "react";
import { useStore } from "tenx/react";

function App() {
  const inputRef = useRef();
  const { todos } = useStore(store, function (state) {
    return {
      todos: state.todos,
    };
  });

  function handleSubmit(e) {
    e.preventDefault();
    // dispatch addTodo action
    store.addTodo(inputRef.current.value);
    inputRef.current.value = "";
  }

  return (
    <>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          ref={inputRef}
          placeholder="What need to be done ?"
        />
      </form>
      <ul>
        {todos.map((todo) => (
          <li key={todo.id}>{todo.title}</li>
        ))}
      </ul>
    </>
  );
}

As you see above code, we import useStore() from "tenx/react". "tenx/react" is another entry point of tenx package. This entry includes many utils/hooks for React, we will discover them later on.

useStore() is a React hook, it takes 2 parameters. The first one is tenx store object, the second one is the state mapping function. The first parameter of the mapping function is the state object, it presents all state values of the store. In this case, we select todos value (its value is an array type, not state mutator) from the state object.

We successfully created addTodo action and consumed todos state. Now we need to create other actions, toggleTodo and removeTodo.

const store = tenx(initial, {
  // addTodo() {},
  toggleTodo({ todos }, id) {
    todos.value = todos.value.map((todo) =>
      todo.id === id
        ? {
            // copy all prev prop values
            ...todo,
            // update completed prop
            completed: !todo.completed,
          }
        : // nothing to change
          todo
    );
  },
  removeTodo({ todos }, id) {
    todos.value = todos.value.filter((todo) => todo.id !== id);
  },
});

We need to update App component as well

return (
  <>
    <ul>
      {todos.map((todo) => (
        <li key={todo.id}>
          <button onClick={() => store.toggleTodo(todo.id)}>toggle</button>
          <button onClick={() => store.removeTodo(todo.id)}>remove</button>
          <span
            style={{
              textDecoration: todo.completed ? "line-through" : "none",
            }}
          >
            {todo.title}
          </span>
        </li>
      ))}
    </ul>
  </>
);

You can find full source here

Introducing computed state

In a previous section we have created some actions for todo app: addTodo, removeTodo, toggleTodo. Let's create component displays some useful info of todo list.

function TodoFilter() {
  const { allTodoCount, activeTodoCount, completedTodoCount } = useStore(
    store,
    function (state) {
      return {
        allTodoCount: state.todos.length,
        activeTodoCount: state.todos.filter((todo) => !todo.completed).length,
        completedTodoCount: state.todos.filter((todo) => todo.completed).length,
      };
    }
  );
  return (
    <>
      <div>All ({allTodoCount})</div>
      <div>Active ({activeTodoCount})</div>
      <div>Completed ({completedTodoCount})</div>
    </>
  );
}

The TodoFilter component above needs 3 states: allTodoCount, activeTodoCount and completedTodoCount. Those states will be re-computed whenever the component renders, evenly no app state updated. By using derived/computed state, you can define some dynamic state computation easily, and computed states only re-compute when your app state changed. Let update the store creating code.

const store = tenx(
  {
    // static states
    // ...
    computed: {
      allTodoCount(state) {
        return state.todos.length;
      },
      activeTodoCount(state) {
        return state.todos.filter((todo) => !todo.completed).length;
      },
      completedTodoCount(state) {
        return state.todos.filter((todo) => todo.completed).length;
      },
    },
  },
  {
    // actions
  }
);

We need to refactor TodoFilter component to consume computed states

function TodoFilter() {
  const { allTodoCount, activeTodoCount, completedTodoCount } = useStore(
    store,
    function (state) {
      return {
        // consuming computed states is the same of normal states, easy ?
        allTodoCount: state.allTodoCount,
        activeTodoCount: state.activeTodoCount,
        completedTodoCount: state.completedTodoCount,
      };
    }
  );
  return (
    <>
      <div>All ({allTodoCount})</div>
      <div>Active ({activeTodoCount})</div>
      <div>Completed ({completedTodoCount})</div>
    </>
  );
}

Let's go into deep dives on computed states. Computed state can be:

computedFunction(state)

A function that returns computed value

tenx({
  computed: {
    activeTodoCount(state) {
      return state.todos.filter((todo) => !todo.completed);
    },
  },
});

Tuple [...dependencyList, combiner]

Each dependency item can be:

  • string (name of static state or other computed states)
  • function (a function returns slice of state)

Combiner is a function that retrieves all values of dependency list and returns combined value.

tenx({
  todos: [],
  filter: "all",
  computed: {
    visibleTodos: [
      "todos",
      "fitler",
      function (todos, filter) {
        if (filter === "active") return todos.filter((todo) => !todo.completed);
        if (filter === "completed")
          return todos.filter((todo) => todo.completed);
        return todos;
      },
    ],
  },
});

How to persist app state

If you want to store your app state to persistence storage (sessionStorage, localStorage, AsyncStorage etc.). We can define init action to add some logics for handling app state changing.

const store = tenx(initial, {
  init(context) {
    const persistedTodos = JSON.parse(localStorage.getItem("appState"));
    if (persistedTodos) {
      context.todos.value = persistedTodos;
    }

    // by using watch api, we can listen when specified state prop changed
    context.watch("todos", function (e) {
      // current value of watched state
      console.log(e.current);
      // previous value of watched state
      console.log(e.previous);
      // save todos to local storage
      localStorage.setItem("appState", JSON.stringify(e.current));
    });
  },
});

An init action will be dispatched when store runs initializing phase. If you define init action is async function, your components cannot consume store state until init action finished.

const store = tenx(initial, {
  async init(context) {
    // todoServer performs loading todos from server
    const persistedTodos = await todoService.load();
    if (persistedTodos) {
      context.todos.value = persistedTodos;
    }

    // by using watch api, we can listen when specified state prop changed
    context.watch("todos", function (e) {
      todoService.save(e.current);
    });
  },
});

In component render function, an error will be thrown if you try to consume in progress store. To handle store initializing progress, you must wrap your App component inside React.Suspense element.

import React, { Suspense } from "react";
import { render } from "react-dom";
import App from "./compoments/App";

render(
  <Suspense fallback="Application loading...">
    <App />
  </Suspense>
);

You can find fully example about lazy state persistence here

Introducing store snapshot (TBD)

Introducing Saga (TBD)

React develop tools (TBD)

Reactotron (TBD)

Writing unit tests (TBD)

Features (TBD)

API Reference (TBD)

Examples

  1. Snake Game
  2. Movie Search
  3. Todo App