What is the best way to log when internal state changes?

For our product, we want to log information when the user performs a certain action in the editor. For example, when the user type ‘@’, we want to show a dropdown, and at the same time we want to log some thing like “a user just type @ to trigger a dropdown in a document with X number of words”. (for context, there are other ways they can trigger the dropdown, e.g. by clicking on a button)

What is the best way to log things like that? I want to make sure that we only log if the action is actually applied, since technically plugins can cancel transactions.

I can think of a few ways:

  1. Store some logging information in the at mention plugin state (for example, the fact that we enter at mention state because user just typed ‘@’). Whenever the editorState is updated, look at the before and after state, extract the at mention plugin state and log if logging information is there. If a change shouldn’t result in logging, we will set the logging info to null in the state.apply hook
  2. Make state.apply only check transaction.getMeta(atMentionPlugin.key) and apply the meta. In appendTransaction hook, perform the same logic as we would have before in state.apply and generate a meta with the new state if the state needs to be updated. Similarly the meta should contain the logging information. Then, in the view layer, we can call view.applyTransaction, get the list of transactions applied, then iterate through them and look at the meta and log based on those

1 seems nice but can lead to excessive logging if we are not careful in state.apply and accidentally return the same state that are not equal (e.g. by doing return {…currentState}). 2 seems closer to the model that we want - only log in response to a transaction that updates the state. But it means that we won’t be using the state.apply hook as much which seems… somewhat sad?

If possible, I think doing this in dispatchTransaction would be the easiest.

1 Like

Looks like you can insert something before your logic that kicks start the dropdown?

Sorry for the bad example, I think the case that’s really tricky here is when the user moves the cursor to before the @ sign, I want to remove the dropdown (and in our case, if the user moves back after the @ sign, we will not show the dropdown again - they have to type @ again to trigger it). That currently happens in the apply hook which I think should be a pure function.

For the case of triggering the dropdown we can do the logging in the same function where we call view.dispatch, but for the case where we update the state in state.apply, it seems bad to trigger IO. I would like to log only when the state is actually applied.

I did something similar (popup when user types “#”)

I keep the “should be shown” data as a boolean in the plugin’s state - it’s the same value that I read to set visible to true or false in the DOM.

As for what triggers it - I use input rules with regex /(^|\s)(#[a-zA-Z\d-_][a-zA-Z\d-_\s]*)$/ and I have the plugin’s state keep track of the results of the regex match, and at what pos the match starts that I use in my logic to detect when it shouldn’t be shown anymore (e.g. moving the cursor before the @ as you mention)

If you do something like that, and in the apply() method just fire off your logger when e.g. oldState.showDropdown === false && newState.showDropdown === true

You shouldn’t be thinking you’d just accidentally return {…currentState} since your apply() method should be responsible for maintaining the correct plugin state anyway.

Thanks for the reply. I think triggering side-effects in the apply() method is not a good idea. Apply() should be a pure function based on the signature. There are also times when we may want to ‘simulate’ a transaction and generate a new state for checking without actually dispatching it (I am doing that with my forked version of input rule plugin to allow undos to go back to the pre-inputrule state) so logging (or triggering other side effect) in apply() can lead to weird issues.

1 Like