crimx/observable-hooks

Using callbacks as refs (`useObservableCallback`)

OliverJAsh opened this issue · 8 comments

https://stackblitz.com/edit/react-ts-a3yjha

This example doesn't currently work. The callback is called but I suspect this happens before the observable is subscribed to. I'm not sure how to fix this—we can't subscribe any earlier? Do you have any ideas?

Perhaps we need a way to subscribe to an observable immediately, e.g. using useConstant?

For context, this is required to implement a pattern such as this: https://reactjs.org/docs/hooks-faq.html#how-can-i-measure-a-dom-node.

import React from "react";
import { render } from "react-dom";

import { useObservableCallback, useLayoutSubscription } from "observable-hooks";

const Comp = () => {
  const [ref, ref$] = useObservableCallback(ob$ => ob$);

  useLayoutSubscription(ref$, console.log);

  return <input ref={ref} />;
};

render(<Comp />, document.getElementById("root"));
crimx commented

Perhaps we need a way to subscribe to an observable immediately, e.g. using useConstant?

This was how the initial version of observable-hooks was implemented. It is unsafe to perform side effects during rendering, especially when it comes to concurrent mode.

The layoutEffect is as early as we can get. Looks like the callback ref is called before the layoutEffect. In this case I could think of three options:

  1. Rethink the logic. See what props/states/context could trigger an element recreation and listen to that instead.
  2. Follow the example in React docs and listen to state changes.
  3. Modify useObservableCallback and use BehaviorSubject instead of Subject.

It is unsafe to perform side effects during rendering, especially when it comes to concurrent mode.

Ah of course, good point.

2. Follow the example in React docs and listen to state changes.

Just to make sure I understand, can you link to the example you're talking about here?

I suspect you're thinking of something like this: https://stackblitz.com/edit/react-ts-tbari6?file=index.tsx. However this is less ideal because it adds an extra render (to set the new element in state).

3. Modify useObservableCallback and use BehaviorSubject instead of Subject.

Interesting idea. Can you elaborate on how that might fix this issue?

I guess this idea would involve a way to store the ref before the observable is subscribed to, and then replay it when the observable is eventually subscribed to.

crimx commented

I suspect you're thinking of something like this: https://stackblitz.com/edit/react-ts-tbari6?file=index.tsx. However this is less ideal because it adds an extra render (to set the new element in state).

Yes and you are right.

I guess this idea would involve a way to store the ref before the observable is subscribed to

BehaviorSubject will keep the latest value even before subscription.

Doesn't BehaviourSubject need an initial value though? I'm not sure what we would use for that in the case of useObservableCallback.

crimx commented

Doesn't BehaviourSubject need an initial value though

Just give it null. Even the ref callback itself may get a null when the element is not ready.

crimx commented

React will call the ref callback with the DOM element when the component mounts, and call it with null when it unmounts. Refs are guaranteed to be up-to-date before componentDidMount or componentDidUpdate fires.

https://reactjs.org/docs/refs-and-the-dom.html#callback-refs

Here is an example https://stackblitz.com/edit/react-ts-xnrmmi?file=index.tsx

Very interesting, thanks.

Is this something you would consider adding to the library?

crimx commented

No I think this pattern is quite rare and specific. A custom hook is enough.