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

Refetch resolves without reloading the state #3399

Closed
1 of 3 tasks
aj-dev opened this issue Apr 3, 2017 · 13 comments
Closed
1 of 3 tasks

Refetch resolves without reloading the state #3399

aj-dev opened this issue Apr 3, 2017 · 13 comments
Labels

Comments

@aj-dev
Copy link
Contributor

aj-dev commented Apr 3, 2017

This is a:

  • Bug Report
  • Feature Request
  • General Query

My version of UI-Router is: v1.0.0-rc.1

Feature Request

Currently the only way to re-resolve data in previously active states is by specifying {reload: true} in ui-sref-opts directive or $state.go() calls. This is a huge limitation where progressive rendering is required and complete state reload is not desirable.

A simplified use case:
Let's say we have 2 states, a parent cards state representing a list of cards which has a couple of resolves and a child cards.card state for editing individual card. Upon card update to the server we return to the list of cards but the change is not reflected because cards state has not been reloaded and fresh list of cards has not been fetched from the server.
Reloading entire state using {reload: true} is undesirable because it re-resolves all dependencies even if some of them don't change at all.

Proposal:
Implement {reFetch: true} which would be similar to {reload: true} to only re-resolve dependencies that are subject to change without reloading the state. I think a good place for this would be resolvePolicy to declare all resolves that should be re-fetched if {reFetch: true} was set via ui-sref-opts directive or $state.go().
For instance:

resolvePolicy: {
    cards: {
        when: 'LAZY',
        async: 'NOWAIT',
        refetch: true
    }
}

Then on transition start we could check if reFetch is true and only re-resolve those resolves that had refetch: true declared in resolvePolicy.

I have created a plunker to illustrate the desired behaviour http://plnkr.co/edit/5CciECkWZiFv9lcBmGZT?p=preview

@mkoczorowski
Copy link

+1

@mkoczorowski
Copy link

did you manage to achieve it somehow?

@aj-dev
Copy link
Contributor Author

aj-dev commented May 28, 2017

@mkoczorowski no, I haven't. I don't think there's any workaround for it but it's a very common use case and I hope that @christopherthielen will consider implementing it.

@crystalcodesoftware
Copy link

I've encountered this problem so many times. Most often when:

  1. A result-set is loaded into a view/state
  2. The user uses form controls in the view to filter the result-set
  3. The user interacts with one of the result-set items such that it requires a reload
  4. The easiest way to facilitate this is a state reload, and they lose their filter form inputs because the whole damn thing reloads

I've "solved" this with $againable; a service which injects an $again method into the result of a promise. The $again method re-calls the promise that returned the result, and you can do as you need. Works wonderfully with refresh functionality.

However, I think that ui-router can solve the re-resolve issue a better way.

$resolvables as a service

Say you have:

resolve: {
    dataSet: function ($http) {
        return $http.get('/some/data/set').then(function (response) {
            return response.data;
        });
    }
}

And your controller is:

controller: function ($scope, dataSet) {
    $scope.dataSet = dataSet;
}

That's fine, but if you need to re-resolve the dataSet, you're fucked. (aside from wrapping it into a service which is overkill IMO, or re-writing the promise logic which is silly IMO)

So instead, we should have a $resolvables service. This would allow the following:

controller: function ($scope, dataSet, $resolvables) {
    $scope.dataSet = dataSet;
    $scope.refresh = function () {
        $resolvables.dataSet().then(function (dataSet) {
            $scope.dataSet = dataSet;
        });
    };
}

Every resolvable property (promise) is available through a $resolvables.{name}() function.

So, you can simply call $resolvables.whatever().then(...) and re-resolve the origin promise to a new value.

Thoughts?

@crystalcodesoftware
Copy link

crystalcodesoftware commented Jun 7, 2017

Alternatively, please pilfer my $again idea.

angular.module('test').service('$againable', function ($q) {
    return function $againable(future) {
        return (function $again() {
            var deferred = $q.defer();
            $q.when(future(), function (result) {
                deferred.resolve(angular.merge(result, { $again: $again }));
            }, function (result) {
                deferred.reject(angular.merge(result, { $again: $again }));
            });
            return deferred.promise;
        })();
    };
});

And then:

resolve: {
    dataSet: function ($againable, $http) {
        return $againable(function () {
            return $http.get('/some/data/set').then(function (response) {
                return response.data;
            });
        });
    }
}

Such that the resolved values have the $again method attached; therefore:

controller: function ($scope, dataSet) {
    $scope.dataSet = dataSet;
    $scope.refresh = function () {
        $scope.dataSet.$again().then(function (dataSet) {
            $scope.dataSet = dataSet;
        });
    };
}

@christopherthielen
Copy link
Contributor

Thing 1

I agree with the intention behind this feature request. I'm am being very careful about feature requests like this because it fundamentally alters one of the core concepts in ui-router. I'm not opposed to enabling a feature like this, but I want to think through the ramifications. I'm also afraid of adding additional mental models, to avoid "fragmentation" of the userbase.

Thing 2

I think that Observables + dynamic parameters can be used (today) to achieve the desired result.

Thing 3

There is a low level (internal) resolve API that could be used to re-resolve individual resolvables.

Resolves are stored on an array of PathNode objects. Each PathNode corresponds to a state. The array of PathNodes represents the path of nested states from root to the deepest state.

The ResolveContext manages dependencies between resolves. Each Resolvable object encapsulates the resolved data and status of the resolve fetch.

You could use this internal API in a component as follows:

function($transition$) {
  var path = $transition$.treeChanges('to');
  var context = new ResolveContext(path);
  var myResolve = context.getResolvable('myResolve');

  // Reset the internal state
  myResolve.resolved = false;
  myResolve.data = undefined;
  myResolve.promise = undefined;

  // re-fetch
  myResolve.get(context).then(result => this.result = result);
}

Of course, these steps should be abstracted into a helper of some sort.

@christopherthielen
Copy link
Contributor

related feature request: #2888 (comment)

@aj-dev
Copy link
Contributor Author

aj-dev commented Jun 13, 2017

@christopherthielen thanks for your response and I understand your concerns about adding "additional mental models" but I think that backwards compatibility shouldn't be an issue and users who never needed this feature should not notice any difference as it would be just one of the optional flags.

As for using internal resolve API, I don't think it's a solution in my case because the way you proposed it, I would have to inject $transition into component controller, write some code for each resolvable and that's the thing I want to avoid because it adds a significant amount of boilerplate code to every component, even when using some sort of a helper. It doesn't look like a very DRY approach. I think UI-Router should handle this transparently if we just provide a refetch: true flag in resolvePolicy of the state config object.

I haven't used observables in UI-Router's context so would appreciate if you could share an example of Observables + dynamic parameters to achieve the desired result.

@dARKaRK
Copy link

dARKaRK commented Aug 16, 2018

Hi @christopherthielen ,
I'm also facing the same use case. I have a page that shows counts of many entities and goes to the entity list when you click on one. Now, If you add or delete an entity and go to the parent state(which showed the count), the counts are not getting updated as they are already resolved.
I tried using your method above by creating a RouteInterceptor class which implements a onStart hook on the transition and then refreshes the resolve.
But I get the error:
Cannot read property 'apply' of null on line myResolve.promise = undefined
It most probably refers to line 118 in source code of core/src/resolve/resolvable.ts:
const invokeResolveFn = (resolvedDeps: any[]) => this.resolveFn.apply(null, resolvedDeps);
I dont know why but my resolveFn always becomes null after data is resolved.
Any thoughts will be helpful.

@p0lar-bear
Copy link

p0lar-bear commented Dec 19, 2018

@dARKaRK If you look at the blame view for resolvable.ts you can see that the value of resolveFn is set to null after the value resolves as part of a change nearly a year ago as of this comment to fix an issue with memory leaks in transitions. I'm not seeing a simple way to keep track of the original resolveFn.

This is a pretty good example of why production code shouldn't rely on internal APIs unless absolutely necessary - @christopherthielen's suggested workaround for re-resolving was broken by this change only a few months after he posted it.

@stale
Copy link

stale bot commented Jan 24, 2020

This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.

This does not mean that the issue is invalid. Valid issues
may be reopened.

Thank you for your contributions.

@stale stale bot added the stale label Jan 24, 2020
@mkoczorowski
Copy link

I have created a new annotation @input$() replacing angular's @input, which gets auto resolved and emitted when a specific dynamic parameter/s change on a state. I will make this available shortly.

@stale stale bot removed the stale label Jan 24, 2020
@stale
Copy link

stale bot commented Jul 25, 2020

This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.

This does not mean that the issue is invalid. Valid issues
may be reopened.

Thank you for your contributions.

@stale stale bot added the stale label Jul 25, 2020
@stale stale bot closed this as completed Aug 8, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants