1

I want to create an observable from a change event that gets fired on a React Native TextInput component. TextInput comes with 2 change props that I'm aware of (onChangeText and onChange). From what I gather, you need to use onChange if you want access to the native event you need to use onChange.

I don't know much about the native event object. I am trying to create an rxjs observable using fromEvent.

First I created a ref in my functional component like this:

const sqftRef = useRef().current

Then I attached this ref to the TextInput component like this:

 <TextInput
    ref={sqftRef} // attach a ref 
    label='Sqft'
    mode='flat'
    textContentType='none'
    autoCapitalize='none'
    keyboardType='numeric'
    autoCorrect={false}
    value={String(formValues.sqft)}
    dense
    underlineColor={colors.colorOffWhite}
    onChangeText={(text) => setText(text)}
    onChange={e => {
        // somehow create an observable from this event ???
    }}
    style={styles.inputStyles}
    theme={inputTheme}
 />

I tried to create an Observable using fromEvent like this but it doesn't work. I get undefined is not an object (evaluating target.addEventListener):

fromEvent(sqftRef, 'onChange').subscribe(value => console.log(value))

I know my approach is all wrong. Hoping someone can point me in the correct direction.

Jo Momma
  • 1,126
  • 15
  • 31

3 Answers3

1

I would emit events you need into a subject, then subscribe to the subject in other parts of your code.

Here's a simple React example that should get you started

function App() {

  const textChange = new Subject<string>();


  useEffect(() => {
    // subscribe to 
    const subscription = textChange.asObservable().subscribe(console.log)
    return () => subscription.unsubscribe()
  }, [])




  // Emit events with a subject
  return <textarea onChange={(e) => {
    textChange.next(e.target.value)
  }}>
  </textarea>

}
render(<App />, document.getElementById('root'));

Check out the example here: https://stackblitz.com/edit/react-ts-akoyfv

Alex Fallenstedt
  • 2,040
  • 1
  • 18
  • 34
  • I tried a few different ways. Initially I got the ref like this: const sqftRef = useRef().current and then it always reference current. That didn't work properly though because as far as I know you have to attach the ref itself to the textinput, not the current value. When I use your solution i get "invalid event target" – Jo Momma Aug 28 '21 at 18:14
  • @JoMomma I've updated my answer with a different idea. It uses React instead of ReactNative, but the principle is still the same. – Alex Fallenstedt Aug 28 '21 at 18:23
  • I don't know why, but when I try a solution like yours, the next() function only fires the first time....It does't complete or error, but every other next() call does nothing. – Jo Momma Aug 28 '21 at 19:07
  • I figured out why. I added it to my answer. It's important to memoize the subscription so it's not re-created when state changes. – Jo Momma Aug 30 '21 at 14:49
1

I think the problem is with assigning the current directly to the sqftRef. Try to define it without current, but use current when creating the Observable, like the following:

const sqftRef = useRef();

Then create the Observable within useEffect to make sure that the DOM is ready:

useEffect(() => {
  fromEvent(sqftRef.current, 'onChange').subscribe((value) =>
    console.log(value)
  );
});
Amer
  • 6,162
  • 2
  • 8
  • 34
  • I caught that too, but now I get an error: 'Invalid event target'. Perhaps it's because it's top level code? Is the ref not attached yet before I try to create a stream? Maybe I will try to set it up in useEffect.... – Jo Momma Aug 28 '21 at 18:29
  • Yes, you should use it within `useEffect`. Sorry, I forgot to mention that. – Amer Aug 28 '21 at 18:29
1

OK, I was able to figure it out with the help of Amer Yousuf and Alex Fallenstedt.

I did something similar to what Alex suggested, modifying his solution for React Native. One reason his solution wasn't working for me is that it is important to use the useRef hook to prevent the Observable from being re-created on each render. If the observable is recreated (on a re-render) and useEffect doesn't run again, then we won't have an active subscription to the newly (re-created) observable (useEffect never runs again). That's why my call to sqft$.next was originally only being called once (the first time until we re-render).

My solution looks like this:

let sqft$ = useRef(new BehaviorSubject(0)).current

useEffect(() => {
        const sub = sqft$.subscribe({
            next: (val) => {
                // just testing stuff out here
                updateForm('sqft', val)
                updateForm('lot', val * 2)
            }
        })
      // this is only relevant to my use case
      if (activeReport) sqft$.next(activeReport.sqft)
      return () => sub.unsubscribe()

   }, [activeReport])

and of course I call this in onChangeText:

onChangeText={(text) => {
   sqft$.next(text)
}}

So this is working right now. I still feel like there may be a better way using onChange(e => ...stuff). I will leave this question open for a little bit in case anyone can break down how to do this using nativeEvent or explain to me how I can access an event off the TextInput component.

Jo Momma
  • 1,126
  • 15
  • 31
  • What is the problem exactly with the solution I suggested, please? – Amer Aug 29 '21 at 06:23
  • It produces an error: "undefined is not an object (evaluating 'target.addEventListener'). I'm not sure why. Do we need to access the nativeEvent? Do we have the wrong event name ('onChange')? I don't know yet but this solution doesn't work. – Jo Momma Aug 29 '21 at 15:48