hookstate icon indicating copy to clipboard operation
hookstate copied to clipboard

Document how to add extension method setWithContext (WAS: Feature request: Allow me to pass meta data to plugins.)

Open doncuomo opened this issue 2 years ago • 5 comments

Hi, have been using hookstate for a while now and it seems pretty cool. We are running into a problem which I think could most easily be solved by allowing us to pass some meta data to plugins. I'll try to explain what we are doing and I'd like to hear your thoughts.

We would like to be able to update the store without updating everything to the server. The easiest way (I think) would be with meta data so we could set something like: skip_backend_save: true. That way we could have a grip on when updating to the server and when not.

What are you doing?!:

We are currently working on pushing our changes to other users on a page. The setup we choose is in short this:

  1. User answers a question
  2. Plugin detects answer and pushes to server
  3. Server saved and broadcasts to other users via websockets (Rails actioncable)
  4. Other users receive new data, we update store.
  5. Store detects changes, back to 2.

5 is where things go south for us and where some meta data would really help us out. Then we could just update the stores with a 'skip_backend_save' flag. Something like this: answer.set(data, meta) would be enough.

We have found a 'kinda hacky' way around it for now. But we would like to know how you feel about this idea.

doncuomo avatar Jul 14 '22 08:07 doncuomo

Really need this too to make use of the plugin functionality.

OtherCroissant avatar Jul 20 '22 12:07 OtherCroissant

Hi, thanks for the request. I had a similar need in my past project and solved it using either of 2 methods:

  1. assign symbol property to a value of a state, limitation - a value should be an object. Assigning symbol property does not impact tracking and rerendering. These are simply ignored by the hookstate by can be used by the plugins
  2. build a global variable - array/stack of context variables, and a function which would push a variable when called and pop when finished. The function needs to accept a custom callback (user's code), which would call it between push and pop. User's code in the callback can access the global variable of context variables

Alternatively, you can wait for the Hookstate-4 extension which will allow to associate metadata. Subscribe to: https://github.com/avkonst/hookstate/issues/34

avkonst avatar Jul 21 '22 09:07 avkonst

My experience with this:

  • We tried option 1, but we also liked removing stuff with setting the special 'Hookstate none object'. But we were unable to add meta data to this none.
  • We now implemented option 2. But it is a bit weird to have a seperate state for communicating to a backend plugin which is directly called by hookstate. When I look at the code, I think it would be reasonably easy to pass a meta object as a argument of the set() and merge() method to the onSet() and onDestroy() callbacks. (But I have little experience with typescript, so therefore I could nog easily PR it).

We are using hookstate 4 now. Very nice that there were no real problems when upgrading to 4! Great backwards compatibility so far.

But we could not figure out how you implmented the metadata sollution you were mentioning. Maybe this would fit our needs already, but we currently miss the documentation for how this should be used.

We now changed to plugin to this, based on numbering all the set calls, but it feels a bit wobbly:

const backendSavePluginId = Symbol("backendSavePlugin");

class BackSavePlugin {
  constructor() {
    this.edition = 0;
    this.denyListedEditions = [];
  }

  onDestroy() {
    this.edition += 1;
  }

  getEdition() {
    return this.edition;
  }

  onSet = (data) => {
    this.edition += 1;

    if (this.denyListedEditions.includes(this.edition)) {
      console.debug("Edition listed as no skip backend save");
      return null;
    }
    /// DO WORK
  };

  denyEdition = (editionId) => {
    this.denyListedEditions.push(editionId);
  };

  skipNextSave = () => {
    this.denyEdition(this.getEdition() + 1);
  };
}

export default (data) => {
  if (data) {
    const [instance] = data.attach(backendSavePluginId);

    if (instance instanceof Error) {
      throw instance;
    }

    return {
      getEdition: instance.getEdition,
      denyEdition: instance.denyEdition,
      skipNextSave: instance.skipNextSave,
    };
  }

  return {
    id: backendSavePluginId,
    init: () => {
      return new BackSavePlugin();
    },
  };
};

OtherCroissant avatar Jul 21 '22 09:07 OtherCroissant

Hookstate-4 is almost API compatible with version 3 while stays on rcX versions. Once the RC suffix is dropped, I will delete deprecated stuff, but before this I need to update the docs and finish few more bits on extensions.

Regarding the extension. Could you please try to implement not a plugin but Extension (new engine for plugins in Hookstate 4, Hookstate 3 attach will get removed)?

Here is an example of Clonable extension:

export interface Clonable {
    clone(): StateValue<this>
}

export function clonable(snapshot: (v: StateValueAtPath) => StateValueAtPath): Extension<Clonable> {
    return {
        onCreate: (sf) => ({
            clone: (s) => () => snapshot(s.get({ noproxy: true }))
        })
    }
}

And this is how to use (extend is optional if you attach only 1 extension):

let state = useHookstate({ a: [0, 1], b: [2, 3] }, () => extend([
    clonable(v => JSON.parse(JSON.stringify(v)))
]))
let clonedValue = state.clone()

As you can see you can add any methods and properties to the State object, for example for you: setFromRemote, setWithContext... These methods will get invoked and you can set a context there into a variable insite the Extension function, call State.set native method and use the context value in the onSet callback to the Extension.

Documentation for extension is still pending, but there are 3 examples available in the plugins/snapshotable folder on master.

avkonst avatar Jul 22 '22 08:07 avkonst

Hi, thanks for the update. I will check it out once I have time. I'm heading on vacation soon :>

doncuomo avatar Jul 22 '22 09:07 doncuomo

Here is the documentation how to write an extension. The example shows how to add setWithContext method https://hookstate.js.org/docs/writing-extension

avkonst avatar Aug 07 '22 07:08 avkonst