Utilizing $resource within a promise sequence to correct the deferred anti-pattern

One challenge I encountered was that when making multiple nearly simultaneous calls to a service method that retrieves a list of project types using $resource, each call generated a new request instead of utilizing the same response/promise/data. After doing some research, I came across a solution which revealed an issue with creating a redundant $q.defer() called the deferred anti-pattern.

The initial code worked well as long as the calls were spaced out sufficiently. However, the consecutive calls did not share the projectTypes data. Additionally, any failed requests to retrieve project types would trigger dfr.reject(), which could be caught by .catch() in the controller.

angular.module('projects')
.factory('projectService', function(notificationService){

    var shared = {};

    var projectResource = $resource(baseApiPath + 'projects', {}, {
        ...,
        getProjectTypes: {
            method: 'GET',
            url: baseApiPath + 'projects/types'
        },
        ...
    });

    var loadProjectTypes = function(){
        var dfr = $q.defer();

        if(shared.projectTypes){
            dfr.resolve(shared.projectTypes);
        }
        else {
            projectResource.getProjectTypes(null,
            function(response){
                shared.projectTypes = response.result.projectTypes;
                dfr.resolve(response);
            },
            function(errResponse){
                console.error(errResponse);
                notificationService.setNotification('error', errResponse.data.messages[0]);
                dfr.reject(errResponse);
            });
        }
        return dfr.promise;
    };

    return {
        shared: shared,
        project: projectResource,
        loadProjectTypes: loadProjectTypes
    };
});

To optimize this code and eliminate unnecessary use of $q.defer(), I made some adjustments:

...
    var projectResource = $resource(baseApiPath + 'projects', {}, {
        ...,
        getProjectTypes: {
            method: 'GET',
            url: baseApiPath + 'projects/types',
            isArray: true,
            transformResponse: function(response){
                return JSON.parse(response).result.projectTypes;
            }
        },
        ...
    });

    var loadProjectTypes = function(){
        return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
    };
...

Despite these improvements, there were still issues with error handling and promise chaining. In the original code, errors were appropriately handled in the catch statement of the controller. However, the updated version lacked similar error propagation. Attempting to replace dfr.reject() and dfr.resolve() with $q.reject() and $q.resolve() respectively proved ineffective.

This raised the question of whether the original implementation actually fell into the category of an anti-pattern or if it was a valid way of utilizing $q.defer(). As I delved deeper into seeking a solution, I realized the need for a consistent approach where the same promise/data was returned regardless of when the method was called. Ensuring proper error handling throughout the promise chain involving $resource posed a challenge yet to be resolved.

Continuing my search for a comprehensive solution, I outlined three main requirements:

Requirement 1: Enable multiple method calls while only triggering a single API request to update all callers with the same data.

Requirement 2: Allow usage of the method result as actual data per the promise specification. For example,

var myStuff = service.loadStuff()
should set myStuff to "stuff".

Requirement 3: Facilitate promise chaining so that any errors within the chain can be managed by a single catch statement at the end. The goal is to have robust error handling across different chains and catches within the application.

Answer №1

It seems that speaking about your problems often leads you to find the solution.

Requirement 1: Ensure only one request is made per method call, which is resolved by fixing the anti-pattern. This fix will always return the $resource result by either caching it or returning and caching it simultaneously.

var loadProjectTypes = function(){
    return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());
};

Requirement 2: Enable using the service method as a promise so that the value of a $scope variable can be set directly to the result of loadProjectTypes(). The updated method allows for simply stating

$scope.theTypes = projectService.loadProjectTypes()
, which automatically fills in the list of types when available, aligning with the promise specification.

Requirement 3: Allow for chaining multiple $resource calls and catching their errors with a single .catch(). By utilizing the $promise of the result from loadProjectTypes within $q.all(), any errors can be caught in a desired catch block.

$q.all([
    ...,
    projectService.loadProjectTypes().$promise,
    ...
])
.then(function(response){
    // handling project types from response[n]
})
.catch(function(errResponse){
    // capturing any errors here
});

Any catches placed at different points will function similarly. Using a .catch() whenever loadProjectTypes() is invoked ensures error handling. Each loader has the flexibility to handle API failures in its unique way. This allows for tailored responses based on where the data is being loaded.

The structures of my service, directive, and controller now appear as follows:

// Code samples used for AngularJS modules and components.

Since the loadProjectTypes (or any other load_____ method) stores the types internally in the originating service, there's no need for additional storage in controllers.

projectService.shared.projectTypes
is universally accessible throughout the application. If all services store results internally, controllers might not require significant logic unless specific view-related actions are necessary. Controllers primarily manage entire pages or modals, while directives and services handle most information and logic.

The question remains open for better solutions. While Jack A.'s suggestion addresses Requirement 1 and potentially 2 and 3, it introduces verbosity to the 'load___' methods. Despite solving some challenges, it can lead to redundant or complex code in practice given slight differences across multiple methods. It's an effective approach but may complicate maintainability in real-world scenarios.

UPDATE (GOTCHA):

After implementing this pattern for a few days, it streamlined our workflow effectively. However, a notable issue emerged when using a method like loadProjectTypes outside of $q.all().

If the load method is used individually in a controller:

// Code snippet in the controller's initialization section
loadProjectTypes()
.$promise
.then(...)
.catch(...);

Upon returning to the controller, issues arise when refreshing - triggering an "undefined .then" error. Subsequent invocations of loadProjectTypes() do not include a $promise; instead, they only return cached project type data. Although baffling, this behavior necessitates handling such scenarios.

return shared.projectTypes || (shared.projectTypes = projectResource.getProjectTypes());

A quick resolution involves incorporating loadProjectTypes().$promise within a $q.all() block:

// Placing this code near the beginning of the controller
$q.all([
    loadProjectTypes().$promise
])
.then(...)
.catch(...);

In situations requiring loading a single item, employing a solitary set within $q.all() eliminates potential issues. While manageable, this adjustment ensures seamless operation in varying contexts, preventing unexpected errors.

Answer №2

I had previously written something quite similar to this, but with a few distinct differences:

  1. My approach involved creating the promise only when the data was already present in the cache and simply returning the native promise whenever an actual request was made.

  2. I introduced a third state for situations where a request for the resource was already in progress.

A more concise representation of the code is shown below:

module.factory("templateService", function ($templateCache, $q, $http) {
    var requests = {};
    return {
        getTemplate: function retrieveTemplate(key, url) {
            var data = $templateCache.get(key);
            
            if (data) {
                var deferred = $q.defer();
                var promise = deferred.promise;
                deferred.resolve({ data: data });
                return promise;
            }
            else if (requests[url]) {
                return requests[url];
            }
            else {
                var req = $http.get(url);
                requests[url] = req;
                req.success(function (data) {
                    delete requests[url];
                    $templateCache.put(key, data);
                });
                return req;
            }
        },
    };
});

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Testing with mount in React Enzyme, the setState method does not function correctly

I've been experimenting with testing this code block in my React App using Jest and Enzyme: openDeleteUserModal = ({ row }: { row: IUser | null }): any => ( event: React.SyntheticEvent ): void => { if (event) event.preventDefault(); ...

What is the best way to guide the user to a different page with PHP once the preventDefault() function in jQuery has been utilized on a

My approach to form validation involves using jQuery, AJAX, and PHP on my website. I utilize PHP for the actual input validation process as I believe it is more secure and prevents users from manipulating scripts through browser source code inspection. Add ...

Sending basic HTML from Express.jsSending simple HTML content from an Express.js

Within my index.html document, I have the following: <input name='qwe'> {{qwe}} I am looking to send {{qwe}} in its literal form, without it being replaced by server-populated variables. How can I achieve this? My initial thought was to ...

DiscordJS bot using Typescript experiences audio playback issues that halt after a short period of time

I am currently experiencing difficulties with playing audio through a discord bot that I created. The bot is designed to download a song from YouTube using ytdl-core and then play it, but for some reason, the song stops after a few seconds of playing. Bel ...

Tips on effectively storing extra object properties across all entries in a MongoDB document group

Currently, I have a node module that involves parsing a .csv file into MongoDB documents. The issue I am encountering is that only the first inserted document contains certain metadata fields, while the rest do not retain these fields. I am seeking guidan ...

Tips for transferring parameters between AJAX requests

Struggling to pass parameters between two different ajax calls, I've noticed that the params only exist within the ajax scope and not outside of it. Is there another way to achieve this without calling from one ajax success section to another ajax? He ...

Guidance that utilizes the scope of a specific instance

I have successfully created a d3.js directive, but I am facing an issue when resizing the window. The values of my first directive seem to be taking on the values of my second directive. How can I separate the two in order to resize them correctly? (both ...

Evaluating Jasmine unit tests using promises with odata

I am new to working with Angular and unit testing in Angular. Our project involves using odata for performing CRUD actions on the database, so we have created a service for this purpose. Here is a snippet of our service code: function DatabaseService($htt ...

Enhancing your Selenium test case strategies for better performance

I've created a test case that compares two arrays, removing matching elements and throwing an exception for non-matching ones. Although it's functional, the test is quite long and messy. Can anyone suggest ways to optimize or improve it? System ...

Error message indicates failure of switch case, resulting in invalid output of

My BMI calculator was working fine until I switched the conditionals to a switch statement for cleaner code. Now, the code is breaking and the result variable isn't being set properly for display. Do you have any ideas on what could be missing here? ...

Populate object values dynamically through function invocations

Currently, I am involved in a project with a VueJS application that incorporates the following helper class and method: class BiometricMap { static get(bioType) { if (!bioType) { return BiometricMap.default(); } const bioTypes = { ...

What is the most efficient method for clearing the innerHTML when dealing with dynamic content?

Is there a more efficient method for cleaning the innerHTML of an element that is constantly changing? I created a function to clean the containers, but I'm not sure if it's the most efficient approach. While it works with the specified containe ...

What are some alternative solutions when functional component props, state, or store are not being updated within a function?

Initially, I need to outline the goal. In React frontend, I display data that aligns with database rows and allow users to perform CRUD operations on them. However, in addition to actual database rows, I include dummy rows in the JSON sent to the frontend ...

mdDialog select an item based on its unique identification number

I am attempting to retrieve the width of a div within an mdDialog. However, the controller for the dialog runs before the HTML content loads, causing the selector to not find anything. Is there a way to employ the window.onload() or document.ready() functi ...

Modifying an HTML attribute dynamically in D3 by evaluating its existing value

I'm facing a seemingly simple task, but I can't quite crack it. My web page showcases multiple bar graphs, and I'm aiming to create a button that reveals or conceals certain bars. Specifically, when toggled, I want half of the bars to vanish ...

Solving filtering issues within React using a combination of conditions

I've been struggling to selectively remove an item from my array. The current filter I'm using is removing too much. Here is the array in question: [ { "domain": "domain1.com", "slug": "moni ...

How to utilize the Ember generate command for an addon

In my Ember addon project, the package.json file looks like this: { "name": "my-addon-ui", "version": "1.0.0", "devDependencies": { "test-addon": "http://example.com/test-addon-1.1.1.tgz", } } Additionally, the package.json file of the depe ...

Monitoring changes in an array of objects using AngularJS's $watch function

Exploring the world of AngularJS, I'm on a quest to solve a challenging issue efficiently. Imagine having an array of objects like this: var list = [ {listprice: 100, salesprice:100, discount:0}, {listprice: 200, salesprice:200, discount:0}, {listpr ...

The text entered in the textbox vanishes after I press the submit button

When a user selects a value in a textbox and clicks the submit button, the selected value disappears. <div class="panel-body" ng-repeat="patient in $ctrl.patient | filter:$ctrl.mrd"> <form> <div class="form-group"> ...

Tips for configuring formik values

index.js const [formData, setFormData] = useState({ product_name: 'Apple', categoryId: '12345', description: 'Fresh and juicy apple', link: 'www.apple.com' }); const loadFormValues = async () => { ...