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

modelfx

v3.0.17

Published

JS(TS) library for decomposited dataflow control in web applications. Can act as M in MVC pattern. Provides the creation of models that are shared components of storage and effects of some data.

Downloads

252

Readme

modelfx

JS(TS) library for decomposited dataflow control in web applications. Can act as M in MVC pattern. Provides the creation of models that are shared components of storage and effects of some data.

npm i modelfx

Model context

Models are using in context.
Contexts are isolated, so data of same model in different contexts are diffrent objects.
Also, model context can hold some application context to use in model effects.

import { createModelContext } from 'modelfx';

const applicationContextThings = {
  config,
  request,
  ...orEverythingYouWant,
};
const modelContext = createModelContext(applicationContextThings);

Model

Model is component of storage and effects of some data.
You name the model, describe the effects and live.

import { createModel } from 'modelfx';

const todoList = createModel(
  // Model Name. That used as unique namespace for storing model things
  'todoList',

  // Model Effects. That produce some external asynchronously work and replace model data.
  // Subscribers will be notified when model setted to pending and when new data is returned.
  (
    // Your things from context creation
    app,
    // Parameters that defines model instance, when you will use model todoList({ user: 'snapdog' })
    params,
    // Actual data of model in current moment
    data,
    // System tools for some features
    tools,
  ) => ({
    async fulfill() {
      return data || (await app.request(`todo/list?user=${params.user}`));
    },

    async update() {
      return await app.request(`todo/list?user=${params.user}`);
    },

    justSetData(data) {
      // Promise is not required
      return data;
    },
  }),

  // Model Live. That happens when model got a first subscriber
  (effects, params, app, { getState, subscribe }) => {
    effects.fulfill();

    // You can refresh your data from backend
    const refreshIntervalId = setInterval(() => {
      effects.update();
    }, 5000);

    // Or interact with other events
    const storageHandler = (event) => {
      effects.justSetData(event.newValue);
    };
    window.addEventListener('storage', storageHandler);

    // Or handle each state change with the subscribe.
    // Internal subscription does not affect the model's death, which occurs when all external subscribers unsubscribe.
    const socket = createSocket().connect('remotetodolist:5111');
    socket.on('connect', () => {
      const unsubscribe = subscribe(() => {
        socket.send(getState());
      });
      socket.on('disconnect', unsubscribe);
    });

    // Model Die. That happens when last external subscriber unsubscribes.
    return ({ clearData }) => {
      clearInterval(refreshIntervalId);
      window.removeEventListener('storage', storageHandler);
      socket.disconnect();

      // You can clear model data. Otherwise, it will be cached in model context.
      // After delay, the data will be cleared if no one will subscribed to the model
      clearData({ delay: 180000 /* ms, default 15000 */ });
    };
  },
);

How to use models

If you imagine model as a data table, then you will select data of rows in it.
Each row is a model instance that you will access by specifying parameters. In other words, model params are key to the instance (to the "row in the table").
When you access the instance, it is instantiated once and lives as long as it has subscribers and will be reused between all calls in the same model context.

const modelContext = createModelContext({ request });

// Call the instance of todoList
const snapdogTodoList = modelContext.dispatch(todoList({ user: 'snapdog' }));

// First subscribe executes model's live. That will fulfill and refresh data, that cause repainting "container" with new todoList
const unsubscribe = snapdogTodoList.subscribe(() => {
  const { data, error, isPending } = snapdogTodoList.getState();

  let content = 'empty';
  if (isPending) {
    content = 'loading';
  } else if (error) {
    content = JSON.stringify(error);
  } else if (data) {
    content = JSON.stringify(data);
  }
  document.getElementById('container').innerText = content;
});

// Also you can dispatch effects
window.addEventListener('focus', () => {
  modelContext.dispatch(todoList({ user: 'snapdog' }).effects.update());
  // or simillar
  snapdogTodoList.effects.update();
});

setTimeout(() => {
  // Last unsubscribe calls model to die. That will clear data
  unsubscribe();
}, 300000);

How to use with React

You can create universal model hook. This hook gives ease way to use model state in react components and causes components to be updated when effects are executed.

export function useModel(selection) {
  const context = React.useContext(ReactModelContext);
  const instance = context.dispatch(selection);
  const [state, setState] = React.useState(instance.getState());

  React.useEffect(() => {
    setState(instance.getState());

    return instance.subscribe(() => {
      setState(instance.getState());
    });
  }, [instance]);

  return state;
}

Setup context

const ReactModelContext = React.createContext();
const modelContext = createModelContext({...});

return (
  <ReactModelContext.Provider value={modelContext}>
    <MainContainer />
  </ReactModelContext.Provider>
);

Now you can use any models easy in react components.

function MainContainer() {
  const userName = useModel(user()).data?.name;
  const snapdogTodoListState = useModel(todoList({ user: userName || '' }));

  const firstTodoItemId = snapdogTodoListState.data?.[0].id;

  const { dispatch } = useContext(ReactModelContext);
  const editFirstItem = useCallback(() => {
    if (!firstTodoItemId) {
      return;
    }
    dispatch(todoItem({ id: firstTodoItemId }).effects.edit('new data'));
  }, [firstTodoItemId]);

  if (snapdogTodoListState.isPending) {
    return <div>loading</div>;
  }

  if (snapdogTodoListState.error) {
    return <div>{snapdogTodoList.error}</div>;
  }

  return <div onClick={editFirstItem}>{snapdogTodoListState.data}</div>;
}

SSR

You can fulfill models on server side and you will already have data in models on client side.
To do this, you should send data in html.

server.js

const modelContext = createModelContext({...});

modelContext.dispatch(todoList({ user: 'snapdog' }).effects.fulfill());

await modelContext.willAllReady();

response.send(`
  <html>
  ...
  <script>
    window.MODEL_STATE = ${JSON.stringify(modelContext.getAllState())};
  </script>
  ...
  </html>
`);

client.js

const modelContext = createModelContext(applicationContextThings, window.MODEL_STATE);
...

Now, you can use models in this context, and it is fulfilled already

Normalize data

If you want to normalize todoList data in two models (list and item), you can do it with tools.normalize

const todoList = createModel(
  'todoList',
  (app, params, data, tools) => ({
    async fulfill() {
      list = await app.request(`todo/list?user=${params.user}`);

      const ids = list.map((item) => {
        tools.normalize(todoItem({ id: item.id }), item);
        return item.id;
      });
      return ids;
    },
  }),
  () => {},
);

const todoItem = createModel(
  'todoItem',
  () => ({}),
  () => {},
);

The todoList now only stores a list of IDs, and the data for each item is stored in todoItem.

const ids = dispatch(todoList({ user: 'snapdog' })).getState().data;
const items = ids.map((id) => dispatch(todoItem({ id })).getState().data);

Optimistic updates

If you want to update data immediately, you could try something like synchronous effect. This will indeed lead to an immediate update, but in case of a request error, it will not be handled.

const todoItem = createModel(
  'todoItem',

  (app, params, data, tools) => ({
    edit(newData) {
      app.request(`todo/item?id=${params.id}`, { post: newData }),
      return newData;
    },
  }),

  () => {},
);

So there is tools.detachEffect. This will cause the request to be executed after the end of the effect call and the todoItem will be additionally updated with the result.

const todoItem = createModel(
  'todoItem',

  (app, params, data, tools) => ({
    edit(newData) {
      tools.detachEffect(() =>
        app.request(`todo/item?id=${params.id}`, { post: newData }),
      );
      return newData;
    },
  }),

  () => {},
);