Comments (13)
What does your observable look like? As the docs stated, useObservableEagerState
should only be used on hot or pure observables. Use useObservableState
if the observable is cold and with side-effects.
from observable-hooks.
They are all BehaviorSubjects. But are being provided as Observables. Here is an example.
const alerts$ = new BehaviorSubject<AlertRecords | null>(null);
const alertsObs$ = alerts$.pipe(distinctUntilChanged());
accessible publically ONLY via an accessor that returns a SINGLE NEVER CHANGING observable instance:
get alerts$(): Observable<AlertRecords | null> {
return alertsObs$;
},
from observable-hooks.
Let me try to state the sequence of events I believe is happening:
- My service is created and exposes an observable, e.g. alerts$ (BehaviorSubject initialized to null)
- My functional component renders and has an
useObservableEagerState
call, e.g.useObservableEagerState(useAlertsService().alerts$)
- It synchronously evaluates to
null
. NOTE theuseObservableEagerState
'suseEffect
has not executed. - The service emits a new value, e.g.
alerts$.next([...])
- The
useObservableEagerState
'suseEffect
executes but ignores all values - The functional component does NOT re-render with the latest results.
from observable-hooks.
Don't know how the alerts$
was implemented. Could you provide a minimum example that reproduce the issue?
- The service emits a new value, e.g.
alerts$.next([...])
- The
useObservableEagerState
'suseEffect
executes but ignores all values
Base on your description, if "useEffect
executes but ignores all values" then the value should be from synchronous emission, which should be captured by the preceding subscription in useState
.
If not, then it means the alerts$
may produce agnostic side-effects. You should use useObservableState
instead.
from observable-hooks.
useObservableState
is also BehaviorSubject
-friendly thanks to #33 .
from observable-hooks.
I haven't gotten around to creating a standalone example-- but in lieu, let me respond to some points.
Don't know how the alerts$ was implemented.
It is the BehaviorSubject implementation from rxjs. https://github.com/ReactiveX/rxjs/blob/master/src/internal/BehaviorSubject.ts
Base on your description, if "useEffect executes but ignores all values" then the value should be from synchronous emission, which should be captured by the preceding subscription in useState.
The useEffect
callback is executed later, after the useObservableEagerState
has completed. As I explained in my sequence of events comment-- after the useObservableEagerState
has completed, the Subject has emitted a new value, and the input$.subscribe
subscription is "skipping" it based on the isAsyncEmissionRef.current
still being false.
The point is you can't guarantee that the value hasn't changed between the time the useObservableEagerState
returns and the useEffect
callback executed since they are contractually asynchronous.
If not, then it means the alerts$ may produce agnostic side-effects.
alert$
is just an observable. I'm not sure I understand what "agnostic side-effects" have to do with the observable design pattern. If you are arguing that I am misusing observables within the React hooks infrastructure, then we could explore that.
On one hand, you could argue that functional components shouldn't be creating "side effects" during their execution. But on the other hand, the point of using Observables is to manage asynchronous events or side-effects. Observables present a asynchronous-like contract, but its actual implementation is largely (entirely?) synchronous.
useObservableState is also BehaviorSubject-friendly thanks to #33 .
The problem with useObservableState
is that it always defaults to its initial value, which is often null
even though the observable already has the value. The component renders but then IMMEDIATELY re-renders with the current value.
I hope to get you a reproducible example sometime!
from observable-hooks.
The useEffect callback is executed later, after the useObservableEagerState has completed. As I explained in my sequence of events comment-- after the useObservableEagerState has completed, the Subject has emitted a new value, and the input$.subscribe subscription is "skipping" it based on the isAsyncEmissionRef.current still being false.
The point is you can't guarantee that the value hasn't changed between the time the useObservableEagerState returns and the useEffect callback executed since they are contractually asynchronous.
If isAsyncEmissionRef.current
is still false
, then it means it is not asynchronous emission.
isAsyncEmissionRef.current
is set to true
synchronously right after the second subcription where useObservableEagerState
gets asynchronous values.
Synchronous values has been captured from the first subscription, that's why we need to skip values from the second one.
alert$ is just an observable. I'm not sure I understand what "agnostic side-effects" have to do with the observable design pattern. If you are arguing that I am misusing observables within the React hooks infrastructure, then we could explore that.
This is not about the observable design pattern but how useObservableEagerState
is implemented. As the warning in the docs stated, since useObservableEagerState
will subscribe the observable more than once, if the observable produces agnostic values on each subscription then there is no way to get a correct initial value safely.
On one hand, you could argue that functional components shouldn't be creating "side effects" during their execution. But on the other hand, the point of using Observables is to manage asynchronous events or side-effects. Observables present a asynchronous-like contract, but its actual implementation is largely (entirely?) synchronous.
useObservableState
works on any type of observable. That's the limitation of useObservableEagerState
.
The problem with useObservableState is that it always defaults to its initial value, which is often null even though the observable already has the value. The component renders but then IMMEDIATELY re-renders with the current value.
Unfortunately because React introduces concurrent mode, we cannot perform synchronous side effects safely. The obserjvable subscription has to happen asynchronously. For BehaviorSubject
, useObservableState
will take it's value
as initial value automatically which will prevent an extra browser paint on the extra re-rendering(just vdom diff).
Suspense
is the official attempt to prevent extra re-rendering on side effects.
from observable-hooks.
I tried to recreate my problem, but am having a hard time understanding a certain behavior. Perhaps you can explain it to me.
Clone https://github.com/Paitum/eager-app then run yarn install
, yarn start
, and show the console in the browser.
To reproduce the behavior, I am purposely emitting a new value immediately after the useObservableEagerState
.
function App() {
const value = useValue();
serviceInstance.fetchData();
NOTE: I made a copy to make it easier to work with.
I can't reproduce the problem, because the state
is somehow being set, and I don't understand where. The only places where state
are set, are in the useState
result and within the next:
of the subscription inside the useEffect
.
So how is the state being set to the newly-emitted value of "Final Value" here: https://github.com/Paitum/eager-app/blob/main/src/use-observable-eager-state.js#L56 ?
from observable-hooks.
So how is the state being set to the newly-emitted value of "Final Value" here
This is because you had strict mode enabled and performed side effects during React rendering.
React is hacking the console.log
so that you don't see the extra loggings🤕.
from observable-hooks.
Thanks. I just used the react create app and it added that. Although Strict must be doing more than hacking console.log, since the behavior changed as well.
Ok, so now when I run it without StrictMode, I see the problem that I was trying to reproduce. The result is "The value is Initial Value" gets rendered.
Now we can have a conversation about "correctness". Let me explain my architecture first.
My application uses a "Service Context", that can provide any component with any service instance via a hook, like useService("alertsService")
. Each service provides an API and encapsulates its behavior, some of which may be synchronous and some of which may be asynchronous. Each service exposes data synchronously via normal accessors or "asynchronously" via RXJS observables. The observable-hooks
library provides us with the means to easily access those ever-changing-values in the UI components.
A pattern we setup is for the service to automatically fetch data ONLY WHEN an observable accessor is requested. So, the alertsService
has an alerts$
accessor that returns the observable for the AlertsData
type. Internally, the service knows if the data has been fetched or not, and goes and fetches it on first request.
Any given component SHOULDN'T know whether a service function or accessor will have a side-effect. That's the point of using observables.
And each service shouldn't need to add any unnecessary logic to support UI constraints-- like making something asynchronous ala setTimeout(() => ..., 0)
.
So that's why I'm wondering why the implementation of useObservableEagerState
ignores new values? What is the harm in storing the value
inside the useEffect
and setState(storedValue)
alongside the isAsyncEmissionRef.current = true;
?
from observable-hooks.
Here's the architecture visually.
observable-hooks
is what makes it easy for UI to get updated when any observable emits... except when it ignores the value.
from observable-hooks.
Thanks for the code!
Although Strict must be doing more than hacking console.log, since the behavior changed as well.
Yes, hooks are invoked twice in strict mode. They try to hide that with hacking console.log
to reduce confusion(which causes more confusions in this case).
A pattern we setup is for the service to automatically fetch data ONLY WHEN an observable accessor is requested. So, the alertsService has an alerts$ accessor that returns the observable for the AlertsData type. Internally, the service knows if the data has been fetched or not, and goes and fetches it on first request.
The pattern you described is very much similar to React's Suspense pattern IMO. With observable-hooks you can easily implement this kind of Stale-While-Revalidate pattern.
A more "Rx" way to achieve this is to make a cold observable that connects to the BehaviorSubject for result-check or triggering side effects.
const fetchData = () => {
console.log('start fetching...')
return new Promise(resolve => {
setTimeout(() => {
resolve('Final Value')
}, 2000)
})
}
function Service() {
const subject$ = new BehaviorSubject('Initial Value');
let fetched = false
const observable$ = new Observable(subscriber => {
if (!fetched) {
fetched = true
fetchData()
.then(value => subject$.next(value))
.catch(error => subject$.error(error))
}
return subject$.subscribe(subscriber)
});
return {
get value$() {
return observable$;
}
}
}
const serviceInstance = new Service()
serviceInstance.value$.subscribe(v => console.log('subscription1', v))
serviceInstance.value$.subscribe(v => console.log('subscription2', v))
So that's why I'm wondering why the implementation of useObservableEagerState ignores new values? What is the harm in storing the value inside the useEffect and setState(storedValue) alongside the isAsyncEmissionRef.current = true;?
useObservableEagerState
should not ignore new values. useObservableState
will triggered an extra React rendering if the observable emits synchronous values. This is because side-effects on React rendering is not safe. An observable subscription is considered a side effect because useObservableState
accepts all kinds of observables.
useObservableEagerState
is an optimization for observables that does not perform side-effects on subscription, so that we can make a synchronous subscription on useState to get the initial value. And since we already have the initial value, it is safe the we ignore the synchronous emissions on the second subscription to prevent extra re-rendering.
Back to the code, the serviceInstance.fetchData();
triggers an emission after the first subscription of useObservableState
where it collects initial values and before the second subscription, it is incorrectly dumped by the useObservableEagerState
. It should make an equality check before dumping the value.
from observable-hooks.
[email protected] published which fixes this issue.
from observable-hooks.
Related Issues (20)
- 老哥多出些rx的文章呗
- rfc: 为 useObservable 添加泛型以匹配多种 Observable HOT 1
- 请问下现在这个库再react18中使用,有多大的风险,有列举有风险的api吗 HOT 1
- 如何使用 useObservableEagerState 获取“从无到有”的 BehaviorSubject 值? HOT 2
- Question - descendent re-rendering? HOT 2
- Unexpected error when using `useSubscription` for a `Subject` HOT 1
- The `BehaviorSubject` of `useObservable` could cause resource leaks HOT 11
- [Bug]: `useObservable*` hooks does not work when observed value is a `Map` instance HOT 3
- 为啥国内作者,文档全是英文呢? HOT 3
- ❓ Question: How can i use useObservable to create a subject HOT 1
- useObservableState: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render. HOT 5
- Provided sourcemaps reference the src folder which isn't distributed HOT 3
- RxJs 7.0 support HOT 2
- Feature proposal: A hook to create a Observable State HOT 2
- 违背hooks原则 HOT 1
- useObservableState(BehaviorSubject) gives undefined when first rendering HOT 2
- useSubscription 会触发 JS 报错导致页面白屏 HOT 27
- Make XHR requests using ObervableResource with a parameter HOT 4
- 如何实现替换react官网的useEventCallback? HOT 11
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from observable-hooks.