Контекст в React и инъекция зависимостей
React

Контекст в React и инъекция зависимостей

Оригинал: React Contexts and Dependency Injection, Jack Hsu

В этой статье я хочу поделиться идеей использования контекста React для реализации инъекции зависимости (Dependency Injection, DI).

Мы рассмотрим две вещи:

  • Что такое контекст в React и как он работает.
  • Почему стоит позаботиться о DI и как реализовать ее с помощью контекста.

React и контекст

Контекст в React — это механизм передачи свойств вниз по дереву всем дочерним компонентам. Разница между контекстом и свойствами в том, что контекст доступен всем потомкам, а свойства — только тому компоненту, которому они переданы.

Рассмотрим небольшой пример:


import React, { PropTypes } from 'react';

class Parent {
  static childContextTypes = {
    get	User: PropTypes.func
  };
  
   getChildContext() {
    return {
      getUser: () => ({ name: 'Bob' })
    };
  }
  
  render() {
    return <Child />;
  }
}

class Child {
  render() {
    return <GrandChild />;
  }
}

class GrandChild {
  static contextTypes = {
    getUser: PropTypes.func.isRequired
  };
  
  render() {
    const user = this.context.getUser();
    return <p>Hello {user.name}!</p>;
  }
}

В компоненте Parent мы реализовали две вещи:

  • Статическое свойство childContextTypes, которое описывает свойства, которые будут доступны всем потомкам.
  • Метод getChildContext(), возвращающий конкретное значение контекста.

Компонент GrandChild реализует статическое свойство contextTypes, описывающее, какие свойства контекста будет использовать компонент (доступны через this.context).

Мощность этого механизма в том, что разрывает зависимость компонента GrandChild от родительского компонента (в данном случае, от компонента Parent).

Обратите внимание: компоненту Child не нужно передавать контекст вниз в компонент GrandChild. В этом отличие от свойств, которые нужно передавать по цепочке вручную.

Инъекция зависимостей

Инъекция зависимостей (Dependency injection) — это шаблон, реализующий принцип Инверсия управления (Inversion of Control).

В последние годы этот шаблон приобрел популярность среди frontend-разработчиков из-за популярности AngularJS.

Смысл DI в том, чтобы иметь зависимости от абстрактных идей, вместо конкретных реализаций. Это позволяет тестировать код и увеличивает возможность повторного использования — мы увидим это позже.

Пример: генератор случайных чисел

Представим компонент RandomNumber, которые отображает случайное число от 1 до max.


class RandomNumber {
  static propTypes = { max: PropTypes.number };
  
  getDefaultProps() {
    return { max: 100 };
  }
  
  render() {
    const num = Math.round(Math.random() * this.props.max);
    return <p>Number: {num}</p>;
  }
}

Текущая реализация связывает компонент с двумя функциями: Math.round и Math.random. Эта связь делает тестирование невозможным, а также невозможно повторно использовать этот компонент, изменив поставщика случайных чисел.

Использование контекста

Давайте посмотрим, как будет выглядеть этот компонент, если мы использует контекст.


class RandomNumber {
  // Specify our dependencies.
  static contextTypes = {
    random: PropTypes.func.isRequired,
    round: PropTypes.func.isRequired 
  };
   
  static propTypes = { max: PropTypes.number };
  
  getDefaultProps() {
    return { max: 100 };
  }
  
  render() {
    const { random, round } =  this.context;
    const num = round(random() * this.props.max);
    return <p>Number: {num}</p>;
  }
}

Это делает тестирование очень простым, поскольку мы можем проконтролировать, что возвращают random и round.


describe('RandomNumber', () => {
  // The context we want to return. We will rebind this in tests.
  let context;
   
  // Test container to provide dependencies.
  class Container {
    static childContextTypes = {
      random: PropTypes.func,
      round: PropTypes.func 
    };

    getChildContext() { return context }

    render() {
      return <div>{this.props.children()}></div>
    }
  }
  
  it('renders number from 1 to max', () => {
    let component, p;
    context = { round: Math.round }; // Binding context.
  
    context.random =  () => 0.9999; // Hard-coded to return 0.9999
    component = TestUtils.renderIntoDocument(
      <Container>{() => <RandomNumber max={10} />}</Container>
    );
    expect(React.findDOMNode(component).textContent).to.match(/10/);
    
    context.random = () => 0.499999;
    component = TestUtils.renderIntoDocument(
      <Container>{() => <RandomNumber max={5000} />}</Container>
    );
    expect(React.findDOMNode(component).textContent).to.match(/2500/);
  });
});

Обобщение

Мы можем расширить наш пример с генератором случайных чисел до любого сервиса. В контексте Flux, мы можем реализовать DI для сторов и экшенов, таким образом компоненты не будут зависеть от конкретных реализаций.


class AppContainer {
  static childContextTypes = {
    userActionCreators: PropTypes.object,
    userStore: PropTypes.object
  };
  
  getChildContext() {
    return {
      // ...
    };
  } 
  
  render() {
    // ...
  }
}

// ... Somewhere deep in our application
class UserAvatar {
  static contextType = { userStore: PropTypes.object.isRequired };
  render() {
    const user = this.context.userStore.getUser();
    return <img src={user.avatarUrl} />;
  }
}

// ... Somewhere else
class UserLogout {
  static contextType = { userActionCreators: PropTypes.object.isRequired };
  render() {
    const logout = this.context.userActionCreators.logout;
    return <a onClick={logout}>Log out</a>;
  }
}

Поднимаемся на ступеньку выше

С помощью генераторов ES7 мы можем создать отличную абстракцию для DI в React.

Давайте рассмотрим два генератора:

  • @inject — содержит contextTypes компонента.
  • @provide — описывает childContextTypes компонента и связывает контекст с данными.

const inject = (injectables) => {
  return (Component) => {
    Component.contextTypes = Object.entries(injectables)
      .reduce((contextTypes, [k, v]) => {
        contextTypes[k] = v.type;
        return contextTypes;
      }, {});

    return class {
      render() {
        return <Component {...this.props} />;
      }
    };
  };
};

const provide = (providing) => {
  return (Component) => class {
    static childContextTypes = Object.entries(providing)
    .reduce((contextTypes, [k, v]) => {
      contextTypes[k] = v.type;
      return contextTypes;
    }, {});

    getChildContext() {
      return Object.entries(providing).reduce((contextTypes, [k, v]) => {
        contextTypes[k] = v.value.call(this);
        return contextTypes;
      }, {});
    }

    render() {
      return <Component {...this.props} />;
    }
  };
};

Здесь используется концепция, которая называется компоненты высших порядков (higher-order components, HoC). Она основывается на функции (декораторе), которая принимает компонент в качестве параметра и возвращает компонент. Это поволяет добавлять дополнительное поведение и метаданные в исходный компонент.

Заключение

В этой статье мы рассмотрели, как можно реализовать инъекцию зависимостей в React при помощи контекста.

Также мы увидели, что инъекция зависимостей может сделать наш код более тестируемым и увеличить возможность повторного использования.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться