Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[NOT A BUG] How to pass sharedValue to custom parser worklet #611

Open
OzymandiasTheGreat opened this issue Feb 2, 2025 · 5 comments
Open
Labels
question Further information is requested

Comments

@OzymandiasTheGreat
Copy link

First let me apologize for posting a question, I don't see that you have discussions enabled.
Also this might be reanimated question, I don't have much experience with either, so I'm not sure.

I'm trying to build a rich text editor based on live markdown, but using custom parser that depends on external state rather than parsing input on every render. To do this I store the external state in a sharedValue that I update from js and expect to see the changed values in parser worklet.
However this doesn't seem to work. No matter what I do the sharedValue.value returns initial value.
Doesn't matter if I access it on js thread (expected, since it's async) or UI (or whatever thread parser runs on) thread.

I'm putting my code below, maybe you can tell me what I'm doing wrong.

export interface RichTextInputChangeEventData extends TextInputChangeEventData {
  fragments?: Fragment[]
}

export interface RichTextInputProps extends TextInputProps {
  onChange?: (e: NativeSyntheticEvent<RichTextInputChangeEventData>) => void
}

type RichTextInputRef = MarkdownTextInput & { toggleBold: () => void }

const RichTextInput = forwardRef<RichTextInputRef, RichTextInputProps>(
  (props, ref) => {
    const inputRef = useRef<MarkdownTextInput>(null)
    const fragments = useSharedValue<Fragment[]>([])

    const toggleBold = useCallback(() => {
      if (fragments.value.length) {
        fragments.value = []
      } else {
        fragments.value = [
          {
            type: DISPLAY_TYPE.BOLD,
            start: 0,
            length:1,
            content: null,
          },
        ]
      }
    }, [])

    // Checking sharedValue
    useAnimatedReaction(
      () => fragments.value,
      (current, previous) => console.log({ current, previous }),
    )

    useImperativeHandle(
      ref,
      () =>
        ({
          ...inputRef.current,
          toggleBold,
        } as RichTextInputRef),
    )

    const onChange = useCallback(
      (e: NativeSyntheticEvent<TextInputChangeEventData>) => {
        props.onChange?.({
          ...e,
          nativeEvent: {
            ...e.nativeEvent,
            fragments: fragments.value,
          },
        })
      },
      [fragments, props.onChange],
    )

    const displayParser = useCallback(
      (text: string) => {
        "worklet"
        console.log("PARSER", fragments.value)
        return fragments.value
          .map((fragment) => {
            let type: MarkdownType
            switch (fragment.type) {
              case DISPLAY_TYPE.BOLD:
                type = "bold"
                break
              default:
                return console.error("Unknown Display type")
            }
            return {
              type,
              start: fragment.start,
              length: text.length,
            }
          })
          .filter((fragment) => !!fragment)
      },
      [fragments],
    )

    return (
      <MarkdownTextInput
        {...props}
        ref={inputRef}
        parser={displayParser}
        onChange={onChange}
      />
    )
  },
)

export default memo(RichTextInput)
@OzymandiasTheGreat
Copy link
Author

Found a solution myself, have to put toggleBold callback in a worklet, like:
runOnRuntime(getWorkletRuntime(), () => {/* update sharedValue */}).
This however doesn't make parser run since no changes to input content. So another question I have is there a way to force parser to rerun? I tried changing key on style change, but that resets the selection and loses focus, which breaks editing.

@tomekzaw
Copy link
Collaborator

tomekzaw commented Feb 2, 2025

Hi @OzymandiasTheGreat, no worries, thanks for the questions.

We're working on something similar with @289Adam289 and we have noticed similar problems.

I store the external state in a sharedValue that I update from js and expect to see the changed values in parser worklet.
However this doesn't seem to work. No matter what I do the sharedValue.value returns initial value.
Doesn't matter if I access it on js thread (expected, since it's async) or UI (or whatever thread parser runs on) thread.

Found a solution myself, have to put toggleBold callback in a worklet, like:
runOnRuntime(getWorkletRuntime(), () => {/* update sharedValue */}).

Exactly that. MarkdownTextInput component creates another worklet runtime which is separate from RN and UI runtimes. If you update sv.value on the RN runtime, it will update only the UI counterpart of the shared value. Under the hood, sv.value = 42; translates to:

runOnUI(() => {
  'worklet';
  sv.value = 42;
})();

So if you want to update a shared value and then read it on the Markdown worklet runtime, you need to call runOnRuntime manually and pass the Markdown worklet runtime as the first argument:

runOnRuntime(getMarkdownRuntime(), () => {
  'worklet';
  sv.value = 42;
})();

or even better

runOnRuntime(getMarkdownRuntime(), (newValue) => {
  'worklet';
  sv.value = newValue;
})(42);

which re-uses the same worklet and serializes only newValue.

FYI, getMarkdownRuntime has been added in #601 and released in 0.1.222.

This however doesn't make parser run since no changes to input content. So another question I have is there a way to force parser to rerun? I tried changing key on style change, but that resets the selection and loses focus, which breaks editing.

Exactly, changing key prop recreates the native component which loses the state (selection, scroll offset, autocomplete, autocorrection etc.) and breaks editing.

Ultimately, we'd like to reformat MarkdownTextInput when any shared value used inside parser worklet changes, similar to how useAninimatedStyle from react-native-reanimated. However, it doesn't work this way yet. Shared values from Reanimated support listeners (addListener/removeListener private API) which can be used to implement this feature – I will try to hack something this week.

As for workaround, what works for us is to regenerate parser worklet reference each time which updates the native parserId prop used under the hood. So instead of using shared values inside a parser worklet, you can just pass the values to the worklet via worklet closure. Note that this requires #607 which has been released in 0.1.228. Previously, we would only reformat the contents if workletHash changes but workletHash doesn't take into account the closure values. Also, I've noticed some problems when updating parser on each MarkdownTextInput render (for instance, sometimes on iOS there will be a visible blink of unformatted text). The reason here is that we call unregisterMarkdownParser too early – basically, expensify::livemarkdown::unregisterMarkdownWorklet is called on the RN runtime before expensify::livemarkdown::getMarkdownWorklet is called on the UI runtime as the result of value change. Here's what seems to fix the problem:

function unregisterParser(parserId: number) {
  setTimeout(() => {
    global.jsi_unregisterMarkdownWorklet(parserId);
  }, 1000);
}

I understand this is not an ideal fix so we'll investigate further.

@tomekzaw tomekzaw added the question Further information is requested label Feb 2, 2025
@OzymandiasTheGreat
Copy link
Author

Thank you very much, that was a very detailed explanation. I have a vague idea of how this all ties together now. Regenerating parser worklet rather than key is both kinda dumb and kinda genius, I'll try it later as I see 0.1.228 is not on npm yet.

@tomekzaw
Copy link
Collaborator

tomekzaw commented Feb 2, 2025

Thank you very much, that was a very detailed explanation.

@OzymandiasTheGreat No worries, thanks for asking.

I'll try it later as I see 0.1.228 is not on npm yet.

Oops, looks like the workflow failed, I'll ask around to see how we can fix that.

@tomekzaw
Copy link
Collaborator

tomekzaw commented Feb 4, 2025

@OzymandiasTheGreat FYI, 0.1.230 is out on npm.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

2 participants