-
Notifications
You must be signed in to change notification settings - Fork 2
Services and Dependency Injection
Components are great and all, but what do we do with data or logic that is not associated with a specific view or that we want to share across components? We build services. Welcome back to Angular: Getting Started, from Pluralsight. Deborah Kurata here, at your service, and in this chapter, we create a service and use dependency injection to inject that service into any component that needs it. Applications often require services such as a product data service or a logging service. Our components depend on these services to do the heavy lifting. Wouldn't it be nice if Angular could serve us up those services on a platter? Well, yes it can. But what are services exactly? A service is a class with a focused purpose. We often create a service to implement functionality that is independent from any particular component, to share data or logic across components or encapsulate external interactions such as data access. By shifting these responsibilities from the component to a service, the code is easier to test, debug, and reuse. In this chapter, we start with an overview of how services and dependency injection work in Angular, then we'll build a service, we'll register that service, and we'll examine how to use the service in a component. We currently have several pieces of our application in place, but we hardcoded our data directly in the product list component. In this chapter, we'll shift the responsibility for providing the product data to a product data service. Let's get started.
Before we jump into building a service, let's take a look at how services and dependency injection work in Angular. In this diagram, our service is here and our component that needs the service is here. There are two ways our component can work with this service. The component can create an instance of the service class and use it. That simple, and it works. But the instance is local to the component, so we can't share data or other resources, and it will be more difficult to mock the service for testing. Alternatively, we can register the service with Angular. Angular then creates a single instance of the service class, called a singleton, and holds onto it. Specifically, Angular provides a built-in injector. We register our services with the Angular injector, which maintains a container of created service instances. The injector creates and manages the single instance, or singleton, of each registered service as required. In this example, the Angular injector is managing instances of three different services, log, math, and myService, which is abbreviated SVC. If our component needs a service, the component class defines the service as a dependency. The Angular injector then provides, or injects, the service class instance when the component class is instantiated. This process is called dependency injection. Since Angular manages the single instance, any data or logic in that instance is shared by all of the classes that use it. This technique is the recommended way to use services because it provides better management of service instances, it allows sharing of data and other resources, and it's easier to mock the services for testing purposes. Now let's look at a more formal definition of dependency injection. Dependency injection is a coding pattern in which a class receives the instances of objects it needs, called its dependencies, from an external source rather than creating them itself. In Angular, this external source is the Angular injector. Now that we've got a general idea of how services and dependency injection work in Angular, let's build a service.
Are we ready to build a service? Here are the steps. Create the service class, define the metadata with a decorator, and import what we need. Look familiar? These are the same basic steps we followed to build our components and our custom pipe. Let's look at the code for a simple service. Here is the class. We export it so the service can be used from any other parts of the application. This class currently has one method, getProducts. This method returns an array of products. Next, we add a decorator for the service metadata. When building services, we often use the Injectable decorator. Lastly, we import what we need, in this case Injectable. Now let's build our service. I've opened the folder for the sample application in the editor once again. Since our service will only provide product data, we'll add it to the products folder. We'll create a new file and call it product.service.ts to follow our naming conventions. We're then ready to create the service class. I bet you can do it in your sleep by now. Export class, and the class name. Since the service provides products, we'll call it ProductService. Next, we decorate the class with the Injectable decorator and we'll add the import statement for that decorator. Now that we have the structure in place, we can add properties or methods to the class as needed. Unless marked private or protected, the properties and methods defined in the class are accessible to any class that uses this service. For our product service, we want a getProducts method that returns the list of products. We strongly type this return value using our IProduct interface, so we need to import this interface. In the next chapter, we'll see how to retrieve the products using HTTP. For now, we'll hardcode them in here. If you're coding along, feel free to copy some products from the products.json file in the api, products folder. Notice that we have no properties defined in this class, so we are not using this particular service to share data, we are using it to encapsulate the data access features. By using this service to provide the list of products, we take the responsibility for managing the data away from the individual component. That makes it easier to modify or reuse this logic. So we're done with our service for now. How do we use it? Well, a service is just really an ordinary class until we register it with an Angular injector. Let's do that next.
As we illustrated in this diagram, we register the service with the Angular injector, and the injector provides the service instance to any component that injects it using the constructor. The injector represented here is the root application injector. But wait, there's more. In addition to the root application injector, Angular has an injector for each component, mirroring the component tree. A service registered with the root application injector is available to any component or other service in the application. A service registered with a specific component is only available to that component and its child or nested components. For example, if a service is registered with the for injection in the product list component and its child, the star component. So, when should you register your service with a root injector versus a component injector? Registering a service with a root injector ensures that the service is available throughout the application. In most scenarios, you'll register the service with the root injector. If you register a service with a component injector, the service is only available to that component and its child or nested components. This isolates a service that is used by only one component and its children, and it provides multiple instances of the service for multiple instances of the component. For example, we have multiple instances of the star component on the Product List page, one for each row. If we had a service that tracks some settings for each star component instance, we would want multiple instances of the service, one for each instance of the component. But this is not a common scenario. With that, the next question is, how? How do we register a service? That depends on which injector we use. We register the service with the root application injector in the service. We pass an object into the Injectable decorator and set the providedIn property to root. We can then access this service from any component or other service in the application. We want to use our product service in several components, so we'll register it with the root application injector. Let's do that now. Here in the service, we add the providedIn property to the Injectable decorator and set it to root. An instance of the product service is then available for injection anywhere in the application. But what if we only wanted to access this service from one component instead? For most scenarios, we'll register our service in the service using the providedIn property. The service is then available to the entire application. To register our service for a specific component, such as the product list component, we register the service in that component, like this. The service is then available to the component and its child components. Note that the providedIn feature is new in Angular version 6. In older code, you'll see the service registered in a chapter like this. The syntax is still valid, however, the recommended practice is to use the new providedIn feature in the service instead. This provides better tree shaking. Tree shaking is a process whereby the Angular compiler shakes out unused code for smaller deployed bundles. We'll talk more about tree shaking later in this tutorial. Now that we've registered the service, let's see how to inject the service so we can use it.
Here again is our diagram. In the last clip we saw how to register the service with the Angular injector. Now we just need to define it as a dependency so the injector will provide the instance in the classes that need it. So, how do we do dependency injection in Angular? Well, the better question is, how do we do dependency injection in TypeScript? The answer is, in the constructor. Every class has a constructor that is executed when an instance of the class is created. If there is no explicit constructor defined for the class, an implicit constructor is used. But if we want to inject dependencies such as an instance of a service, we need an explicit constructor. In TypeScript, a constructor is defined with a constructor function. What type of code normally goes into the constructor function? As little as possible. Since the constructor function is executed when the component is created, it is primarily used for initialization and not for code that has side effects or takes time to execute. We identify our dependencies by specifying them as parameters to the constructor function, like this. Here we define a private variable to hold the injected service instance. We create another variable as the constructor parameter. When this class is constructed, the Angular injector sets this parameter to the injected instance of the requested service. We then assign the injected service instance to our local variable. We can then use this variable anywhere in our class to access service properties or methods. This is such a common pattern that TypeScript defined a shorthand syntax for all of this code. We simply add the accessor keyword, such as private here, to the constructor parameter. Then this is a shortcut for declaring this variable, defining a parameter, and setting the variable to the parameter. Neat. You'll see this technique used throughout the Angular documentation and other code examples. Ready to give it a try? We want to use our service to get products in the product list component, so we'll define our product service as a dependency in the product list component. We don't have to add the providers array here because the product service is already registered. All we need is a constructor, and we already have one here. We'll use the shorthand syntax to define the dependency, private productService. Then, because we are using TypeScript, we type colon and the type, which is ProductService. Note that the accessor doesn't have to be private. The shorthand syntax works with public and protected as well. So now we have a syntax error here. I bet you know why. Yep, we need to import ProductService so we can use it as the data type here. Now, when an instance of the product list component is created, the Angular injector injects in the instance of the ProductService. We are at the point now where we can actually use the ProductService. First, let's delete the hardcoded products from here. We'll instead get them from the service. Now the question is, where should we put the code to call the service? One thought might be to put it in the constructor. But ultimately our product service will go out to a back-end server to get the data. We don't want all of that executed in the constructor. Other thoughts? Remember our discussion about lifecycle hooks? Earlier in this tutorial, we said that the OnInit lifecycle hook provides a place to perform any component initialization, and it's a great place to retrieve the data for the template. Let's use the OnInit lifecycle hook. We want to set the product's property to the products returned from our service. To call the service, we use our private variable containing the injected server instance. We then type a dot and the name of the method we want to call. Notice how IntelliSense helps us with all of this. There is a small problem with our code at this point. Since the constructor is executed before the ngOnInit, the list of products will be empty here. We want to set our filtered list of products to this list of products, so we need to move this line of code to the ngOnInit as well. Let's make one more little change. Let's remove the default listFilter value so we'll see all of the products in the list. We should be all set to see our result in the browser. And here are our products. Yay! Notice that we have more products displayed now because I hardcoded more products into the service. Let's finish up this chapter with some checklists we can use as we build our services.
We build a service using the same techniques as when we build components and custom pipes. We start by creating the service class, we specify a clear class name appropriate for the services it provides, use PascalCasing where each word of the name is capitalized, append Service to the name, and don't forget the export keyword. We then decorate the service class with the Injectable decorator. Don't forget the app prefix, and since the decorator is a function, follow it with open and closing parentheses, and be sure to define the appropriate imports. The first step to registering a service is to select the appropriate level in the injector hierarchy that the service should be registered. Use the root application injector if the service is shared throughout the application. If only one component and its children need the service, register it with that component's injector. Pick one or the other, not both. Register a service with the root injector using the Injectable decorator of the service, set the providedIn property to root, register a service for a specific component and its children using its component decorator, use the provider's property to register the service, and any class that needs the service, specify the service as a dependency. Use a constructor parameter to define the dependency. The Angular injector will inject an instance of the service when the component is instantiated. This chapter was all about services. We began with an overview of how services and dependency injection work in Angular, then we walked through how to build a simple service. We defined how and where to register the service with Angular, and we examined how to define the service as a dependency so the service instance is injected into any class that needs it. In this chapter, we built the product data service, so our product list component no longer has hardcoded products. Next up, we'll see how to modify the service to retrieve data using HTTP.