dsuryd / dotNetify-react-template

Real-time React SPA template using dotNetify.
https://dotnetify.net/react
Apache License 2.0
203 stars 57 forks source link

How to use async Observables correctly with dotNetify #72

Closed epsmae closed 3 years ago

epsmae commented 3 years ago

Question:

What is the correct way to use async observable?

Used async sample

I simplified your sample like the following

RecentActivity = Observable
  .Interval(TimeSpan.FromSeconds(2))
  .StartWith(0)
  .Select(_ => GetRandomEmployee(employeeService))
  .Select(employee => new Activity
  {
     Id = employee.Id,
     PersonName = employee.FullName,
     Status = _activities[_random.Next(1, 6)]
  });

Now I want to use an async method, I tried the following approaches without success.

Attempt 1:

RecentActivity = Observable
 .Interval(TimeSpan.FromSeconds(2))
 .StartWith(0)
 .Select(_ => GetRandomEmployee(employeeService))
 .Select(employee => Observable.FromAsync(async () => await LoadActivity(employee)))
 .Merge(); 

Attempt 2:

RecentActivity =
  from l in Observable.Interval(TimeSpan.FromSeconds(2))
  from a in Observable.FromAsync(() => LoadActivity(GetRandomEmployee(employeeService)))
  select a;

The following errors occur:

Uncaught TypeError: Cannot read property 'map' of null
    at RecentActivities (RecentActivities.tsx:52)
    at renderWithHooks (react-dom.development.js:16260)
    at updateFunctionComponent (react-dom.development.js:18347)
    at beginWork$1 (react-dom.development.js:20176)
    at HTMLUnknownElement.callCallback (react-dom.development.js:336)
    at Object.invokeGuardedCallbackDev (react-dom.development.js:385)
    at invokeGuardedCallback (react-dom.development.js:440)
    at beginWork$$1 (react-dom.development.js:25780)
    at performUnitOfWork (react-dom.development.js:24695)
    at workLoopSync (react-dom.development.js:24671)

The following works but is not the desired way

RecentActivity = Observable
  .Interval(TimeSpan.FromSeconds(2))
  .StartWith(0)
  .Select(_ => GetRandomEmployee(employeeService))
  .Select(employee => LoadActivity(employee).Result);

As a reference just an async method:

private async Task<Activity> LoadActivity(EmployeeModel employee)
{
  await Task.Delay(10);

  return new Activity
  {
      Id = employee.Id,
      PersonName = employee.FullName,
      Status = _activities[_random.Next(1, 6)],
  };
}
dsuryd commented 3 years ago

This works:

.Select(employee => Observable.FromAsync(() => LoadActivity(employee)))
.Merge()
epsmae commented 3 years ago

@dsuryd No this only flickers the dashboard once and results in an empty dashboard --> "Uncaught TypeError: Cannot read property 'map' of null" as mentioned above.

dsuryd commented 3 years ago

It's throwing that error because the initial value of RecentActivity is null because the async data is formulated after the fact. So you need to set the initial value to non-null, something like below:

RecentActivity = Observable
   .Interval(TimeSpan.FromSeconds(2))
   .StartWith(0)
   .Select(_ => GetRandomEmployee(employeeService))
   .Select(employee => Observable.FromAsync(() => LoadActivity(employee)))
   .Merge()
   .StartWith(new[] { new Activity() });
epsmae commented 3 years ago

@dsuryd thanks for your answer. This displays an empty activity for the start, not really what I desire.

One workaround would be to check in the client code if the objetc is "valid". Is there another way achieve this without changing the client code?

dsuryd commented 3 years ago

Updating the client code to ignore null/empty value is a trivial solution. Otherwise, make it synchronous only when populating the initial value.

epsmae commented 3 years ago

@dsuryd

.StartWith(new[] { LoadInitialActivity.Result }); Is blocking the UI so I will go with: .Select(employee => Observable.FromAsync(() => LoadActivity(employee))) And check for null in the client.

Thanks alot.