Обертка для контекста
Хорошей практикой является то, что наш контекст - это не просто простой объект, а еще имеет интерфейс, который позволяет нам хранить и извлекать данные
// dependencies.js
export default {
data: {},
get(key) {
return this.data[key];
},
register(key, value) {
this.data[key] = value;
}
}
Затем, если мы вернемся к нашему примеру, самый верхний компонент App может выглядеть так:
import dependencies from './dependencies';
dependencies.register('title', 'React in patterns');
class App extends React.Component {
getChildContext() {
return dependencies;
}
render() {
return <Header />;
}
}
App.childContextTypes = {
data: PropTypes.object,
get: PropTypes.func,
register: PropTypes.func
};
И наш компонент Title получает его данные через контекст:
// Title.jsx
export default class Title extends React.Component {
render() {
return <h1>{ this.context.get('title') }</h1>
}
}
Title.contextTypes = {
data: PropTypes.object,
get: PropTypes.func,
register: PropTypes.func
};
В идеале мы не хотим указывать contextTypes каждый раз, когда нам нужен доступ к контексту. Эта деталь может быть обернута компонентом высокого порядка(HOC). И даже более того, мы можем написать вспомогательную функцию, которая более лучше описана, то есть, вместо того, чтобы напрямую обращаться к контексту this.context.get ('title'), мы просим компонент высокого порядка получить то, что нам нужно, и передать его в качестве свойств нашему компоненту. К примеру:
// Title.jsx
import wire from './wire';
function Title(props) {
return <h1>{ props.title }</h1>;
}
export default wire(Title, ['title'], function resolve(title) {
return { title };
});
Функция Wire принимает сначала компонент React, затем массив со всеми необходимыми зависимостями (которые уже зарегистрированы), а затем функция, которую я хотел бы назвать mapper. Она получает то, что хранится в контексте как необработанные данные, и возвращает объект, который является фактическим реквизитом для нашего компонента Title. В этом примере мы просто передаем то, что получим - переменная строки заголовка. Однако в реальном приложении это может быть коллекция хранилищ данных, настройка или что-то еще. Итак, хорошо, что мы передаем именно то, что нам нужно, и не загрязняем компоненты данными, которые им не нужны.
Вот как выглядит функция wire:
var dependencies = {};
export function register(key, dependency) {
dependencies[key] = dependency;
}
export function fetch(key) {
if (key in dependencies) return dependencies[key];
throw new Error(`"${ key } is not registered as dependency.`);
}
export function wire(Component, deps, mapper) {
return class Injector extends React.Component {
constructor(props) {
super(props);
this._resolvedDependencies = mapper(...deps.map(fetch));
}
render() {
return (
<Component
{...this.state}
{...this.props}
{...this._resolvedDependencies}
/>
);
}
};
}
Мы будем хранить зависимости в глобальной переменной зависимостей (она глобальна для нашего модуля, а не на уровне приложения)
Затем мы экспортируем две функции register и fetch для записи и чтения.
Это немного похоже на реализацию setter и getter против простого объекта JavaScript.
Затем у нас есть функция wire, которая принимает наш компонент React и возвращает компонент высокого порядка.
В конструкторе этого компонента мы резолвим зависимости, а затем при рендеринге исходного компонента мы передаем их как реквизиты.(props).
Мы следуем той же схеме, где мы описываем, что нам нужно (аргумент deps), и извлекаем необходимые реквизиты с помощью функции mapper.
Имея помощник di.jsx, мы снова можем регистрировать наши зависимости в точке входа нашего приложения (app.jsx) и вводить их везде (Title.jsx), которые нам нужны.
// app.jsx
import Header from './Header.jsx';
import { register } from './di.jsx';
register('my-awesome-title', 'React in patterns');
class App extends React.Component {
render() {
return <Header />;
}
}
// Header.jsx
import Title from './Title.jsx';
export default function Header() {
return (
<header>
<Title />
</header>
);
}
// Title.jsx
import { wire } from './di.jsx';
var Title = function(props) {
return <h1>{ props.title }</h1>;
};
export default wire(Title, ['my-awesome-title'], title => ({ title }));
Если мы посмотрим на файл Title.jsx, мы увидим, что актуальный компонент и wiring могут находиться в разных файлах. Таким образом, компонент и функция mapper становятся легко тестируемыми.