Provide/Inject en Vue.js

0
6809

Índice de contenidos

1. Introducción

En este tutorial aprenderemos a usar el sistema de Provide/Inject de Vue.js con Typescript.

Responderemos a algunas preguntas como:

  • ¿Para qué sirve?
  • ¿Para qué no sirve?
  • ¿Cuándo usarlo?

Este tutorial asume un conocimiento intermedio de Vue.

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3)
  • Sistema operativo: macOS Mojave 10.14.4
  • Entorno de desarrollo: Visual Studio Code
  • Versión de Vue: 2.6.10
  • Versión de Typescript: 3.4.3

3. El problema

Si has usado Vue (o cualquier otro framework de front basado en componentes), probablemente hayas sufrido este problema:

  • Tengo un componente padre con propiedad nombre.
  • Tengo un componente hijo, que no quiere saber el nombre de su padre.
  • Tengo un componente nieto, que necesita saber el nombre de su abuelo.

 

¿Cómo le pasamos al nieto, el nombre del padre, sin pasar por el hijo?

4. Provide/Inject

Vue nos proporciona de manera nativa la inyección de dependencias en componentes, llamada Provide/Inject .

  • Un componente puede proveer de algo (un valor, un objeto o una función).
  • Cualquier componente hijo, nieto, etc puede inyectar (leer) ese algo.

 

Ventajas:

  • Centralización de las instancias a usar por los componentes.
  • Inversión de control.
  • Facilita el testing de componentes.

 

Desventajas:

  • No es reactivo*.
  • Puede ocultar el origen de los elementos inyectados.
  • No sigue el estándar prop/event de Vue.

 

Podrías pensar que, todo eso está muy bien, pero si no es reactivo, ¿para qué me sirve? Te recomiendo seguir leyendo hasta la sección de Reactividad.

5. Uso real

En este ejemplo veremos un uso real y útil de Provide/Inject.

Tenemos un par de servicios, LanguageService  y DateService , que uno de nuestros componentes necesita utilizar.

src/services/language

export interface LanguageService {
  getLanguage(): string;
}

export class NavigatorLanguageService implements LanguageService {
  getLanguage(): string {
    return navigator.language;
  }
}

src/services/date

export interface DateService {
  getDate(): Date;
}

export class BrowserDateService implements DateService {
  getDate(): Date {
    return new Date();
  }
}

Podríamos crear la instancia en el mismo componente que la va a utilizar, pero no estaríamos siguiendo el concepto de inversión de control. Y nuestro testing sería menos agradable de hacer.

Crear un provider es tan sencillo como:

src/providers/ServiceProvider.vue

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import { LanguageService } from "../services/language/LanguageService";
import { NavigatorLanguageService } from "../services/language/NavigatorLanguageService";
import { DateService } from "../services/date/DateService";
import { BrowserDateService } from "../services/date/BrowserDateService";
import { Person } from "../models/Person";

@Component({ name: "ServiceProvider" })
export default class ServiceProvider extends Vue {
  @Provide()
  languageService: LanguageService = new NavigatorLanguageService();

  @Provide()
  dateService: DateService = new BrowserDateService();
}
</script>

La creación de las instancias de nuestros servicios va a estar centralizada en este proveedor. Si algún día estos cambian, solo tendremos que cambiar este archivo.

Todos los componentes que estén dentro del context del <slot>  tendrán acceso a los servicios.

Para dar acceso al componente a los servicios provistos, sólo tenemos que:

src/App.vue

<template>
  <div id="app">
    <ServiceProvider>
      <Child />
    </ServiceProvider>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import ServiceProvider from "./providers/ServiceProvider.vue";
import PersonProvider from "./providers/PersonProvider.vue";
import Child from "./components/Child.vue";
import PersonInfo from "./components/PersonInfo.vue";

@Component({
  name: "App",
  components: { ServiceProvider, Child, PersonProvider, PersonInfo }
})
export default class App extends Vue {}
</script>

Ahora, todos los componentes dentro de <ServiceProvider>  tendrán acceso a los servicios.

Si vemos el código de <Child> , veremos que es muy sencillo, a propósito:

src/components/Child.vue

<template>
  <div>
    <GrandChild />
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import GrandChild from "./GrandChild.vue";

@Component({ name: "Child", components: { GrandChild } })
export default class Child extends Vue {}
</script>

Child , el hijo, no utiliza los servicios que su padre ha proveído. Inyectar es totalmente opcional.

Si vemos GrandChild , el nieto, por otro lado:

src/components/GrandChild.vue

<template>
  <section>
    <div>
      Your language is
      <span>{{ languageService.getLanguage() }}</span>
    </div>
    <div>
      Today is
      <span>{{ dateService.getDate() }}</span>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue, Inject } from "vue-property-decorator";
import { LanguageService } from "../services/language/LanguageService";
import { DateService } from "../services/date/DateService";
import { Person } from "../models/Person";

@Component({ name: "GrandChild" })
export default class GrandChild extends Vue {
  @Inject()
  languageService!: LanguageService;

  @Inject()
  dateService!: DateService;
}
</script>

Podemos ver que inyectar unas instancias de un proveedor es muy sencillo.

Al poner !  al final una variable le estamos diciendo a Typescript que ésta siempre va a estar inicializada.

Y si vemos los tests de GrandChild , veremos cómo proveer al componente de unas instancia de servicios creadas específicamente para facilitar el test.

src/components/tests/GrandChild.spec.ts

import { shallowMount } from '@vue/test-utils';
import GrandChild from '../GrandChild.vue';
import { LanguageService } from '@/services/language/LanguageService';
import { DateService } from '@/services/date/DateService';

class LanguageServiceStub implements LanguageService {
  constructor(private readonly expected: string) {}
  getLanguage(): string {
    return this.expected;
  }
}

class DateServiceStub implements DateService {
  constructor(private readonly expected: Date) {}
  getDate(): Date {
    return this.expected;
  }
}

describe('GrandChild', () => {
  it('should render the language', () => {
    const given = 'EXPECTED LANGUAGE';
    const expected = 'EXPECTED LANGUAGE';

    const wrapper = shallowMount(GrandChild, {
      provide: {
        languageService: new LanguageServiceStub(given),
        dateService: new DateServiceStub(new Date())
      }
    });

    expect(wrapper.html().includes(expected)).toBe(true);
  });

  it('should render the date', () => {
    const given = new Date('10-10-2010');
    const expected = 'Sun Oct 10 2010';

    const wrapper = shallowMount(GrandChild, {
      provide: {
        languageService: new LanguageServiceStub(''),
        dateService: new DateServiceStub(given)
      }
    });

    expect(wrapper.html().includes(expected)).toBe(true);
  });
});


6. Reactividad

Si intentamos hacer Provide()  de un string  que se puede modificar por un <input>:

<template>
  <div>
    <label for>
      Person Name
      <input type="text" v-model="name">
    </label>
    <slot></slot>
  </div>
</template>
<script>
@Component({ name: 'PersonProvider' })
export default class PersonProvider extends Vue {
  @Provide()
  name: string = "Thor"
}
</script>

Y lo inyectamos de esta manera:

<template>
  <div>
    Your name is
    <span>{{ name }}</span>
  </div>
</template>
<script>
@Component({ name: "PersonInfo" })
export default class PersonInfo extends Vue {
  @Inject()
  name!: string;
}
</script>

La impresión inicial será que el nombre se está renderizando. Pero al cambiar el valor del input, veremos que PersonInfo  no se actualiza.

Esto es porque Provide/Inject no es reactivo con tipos primitivos (number, string, boolean, etc). Si queremos proveer de manera reactiva, debemos hacerlo con una propiedad observable, como una propiedad de un objeto. Por ejemplo:

Teniendo la clase Person:

src/models/Person.ts

export class Person {
  constructor(public name: string, public age: number) {}
}

Y el proveedor PersonProvider:

src/providers/PersonProvider.vue

<template>
  <div>
    <label for>
      Person Name
      <input type="text" v-model="person.name" />
    </label>
    <label for>
      Person Age
      <input type="number" v-model="person.age" />
    </label>
    <hr />
    <slot></slot>
  </div>
</template>

<script lang="ts">
import { Component, Vue, Provide } from "vue-property-decorator";
import { Person } from "../models/Person";

@Component({ name: "PersonProvider" })
export default class PersonProvider extends Vue {
  @Provide()
  person: Person = new Person("Thor", 18);
}
</script>

Podemos inyectar el objeto en PersonInfo.vue:

src/components/PersonInfo.vue

<template>
  <section>
    <div>
      Your name is
      <span>{{ person.name }}</span>
      and your age is
      <span>{{ person.age }}</span>
    </div>
  </section>
</template>

<script lang="ts">
import { Component, Vue, Inject } from "vue-property-decorator";
import { Person } from "../models/Person";

@Component({ name: "PersonInfo" })
export default class GrandChild extends Vue {
  @Inject()
  person!: Person;
}
</script>

E incluso podemos encapsular un Provider dentro de otro Provider en App.vue

src/App.vue

<template>
  <div id="app">
    <ServiceProvider>
      <PersonProvider>
        <Child />
        <PersonInfo />
      </PersonProvider>
    </ServiceProvider>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import ServiceProvider from "./providers/ServiceProvider.vue";
import PersonProvider from "./providers/PersonProvider.vue";
import Child from "./components/Child.vue";
import PersonInfo from "./components/PersonInfo.vue";

@Component({
  name: "App",
  components: { ServiceProvider, Child, PersonProvider, PersonInfo }
})
export default class App extends Vue {}
</script>

Si ahora modificamos el valor del <input> , veremos cómo el componente PersonInfo se actualiza.

Si necesitas más reactividad, échale un vistazo a Vue Reactive Provide.

7. Conclusiones

Provide/Inject en Vue.js nos permite pasar información entre componentes de una manera más limpia que con props , pero tiene unas limitaciones claras.

Su uso está recomendado para la creación de librerías de componentes o plugins. Si no te convence para tu proyecto, échale un vistazo a Vuex.

Todo el código del tutorial está disponible en Github.

8. Referencias

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad