Контекст (Context)

Данная глава посвящена рассмотрению влияния типизации на такой механизм как контекст, который хотя и не привносит ничего, что на текущий момент могло бы вызвать удивление, все же имеет один не очевидный момент.

Определение контекста

Определение контекста осуществляется при помощи универсальной функции определяющей один обязательный параметр выступающий в качестве инициализационного значения и возвращающей объект контекста createContext<T>(initialValue: T): Context<T>. В случаях когда инициализационное значение в полной мере соответствует предполагаемому типу, чьё описание не включает необязательных членов, то аргументы типа можно или даже нужно не указывать. В остальных случаях это становится необходимостью.

ts
import {createContext } from "react";


/**Аргумента типа не требуется */

interface AContext {
    a: number;
    b: string;
}

         /**[0]             [1][2] */
export const A = createContext({
    a: 0,
    b: ``
});

/**
 * Поскольку при определении контекста [0]
 * в качестве обязательного аргумента было
 * установлено значение [2] полностью соответствующее
 * предполагаемому типу AContext, аргумент типа
 * универсальной функции можно опустить [1].
 * 
 */


 /**Требуется аргумент типа */

interface BContext extends AContext {
    c?: boolean;
}

                               /**[0]     [1] */
export const B = createContext<BContext>({
    a: 0,
    b: ``
});


/**
 * Так как инициализационное значение [1]
 * лишь частично соответствует предполагаемому
 * типу BContext тип объекта контекста необходимо
 * конкретизировать при помощью аргументов типа [0]
 */


/**Требуется аргумент типа */

                                    /**[0]       [1] */
export const C = createContext<BContext | null>(null);

/**
 * По причине отсутствия на момент определения 
 * инициализационного значения оно заменено на null [1],
 *, что требует упомянуть при конкретизации типа значения [0].
 */

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

Поскольку функциональные и классовые компоненты подразумевают различные подходы для взаимодействия с одними механизмами реализуемыми React, после регистрации контекста в react дереве работа с ним зависит от вида компонента нуждающегося в поставляемых им данных.

После определения контекста необходимо зарегистрировать в react дереве предоставляемого им Provider установив ему необходимые данные.

ts
import React, {createContext } from "react";

/**0 */
const Context = createContext({
    status: ``,
    message: ``
});

/**
 * [0] определение контекста.
 */

   /**[1] */
const App = () => (
        /**[2]                                 [3] */
    <Context.Provider value={{status: `init`, message: `React Context!`}}>

    </Context.Provider>
)

/**
 * [1] определяем компонент представляющий точку входа
 * в приложение и регистрируем в его корне Provider [2]
 * которому при инициализации устанавливаем необходимое значение [3].
 */

На следующем шаге определим классовый компонент и добавим его в ветку, корнем которой является определенный на предыдущем шаге Provider.

ts
const App = () => (
    <Context.Provider value={{status: `init`, message: `React Context!`}}>
        <ClassComponent /> 
    </Context.Provider>
)

class ClassComponent extends Component {
    render(){
        return (
        );
    }
}

Поскольку компонент является классовым, единственный способ добраться до предоставляемых контекстом данных заключается в создании экземпляра Consumer, который в качестве children ожидает функцию обозначаемую как render callback. Данная функция определяет единственный параметр принадлежащий к типу данных передаваемых с помощью контекста, а возвращаемое ею значение должно принадлежать к любому допустимому типу представляющему элемент React дерева.

ts
class ClassComponent extends Component {
    render(){
        return (
                  /**[0]        [1]               [2] */
            <Context.Consumer >{data => <span>{data.message}</span>}</Context.Consumer>
        );
    }
}

/**
 * Поскольку компонент ClassComponent является
 * классовым, единственный вариант получить в нем
 * данные предоставляемые контекстом заключается
 * в создании экземпляра Consumer, который в качестве
 * children ожидает функцию обозначаемую как render callback
 * единственный параметр которой принадлежит к типу данных, а
 * возвращаемое значение должно принадлежать к одному из допустимых
 * типов предполагаемых React.
 */

Случаи предполагающие определение render callback вне Consumer потребуют указания аннотации типа его единственному параметру, тип для которого лучше объявить в месте определения контекста. Если данные инициализации в полной мере соответствуют ожидаемому типу, то его получение проще выполнить с помощью механизма запроса типа, чем описывать вручную. В остальных случаях описание потребуется выполнять самостоятельно.

ts
        /**0 */
let initialValue = {
    status: ``,
    message: ``
};
const Context = createContext(initialValue);

      /**[1]         [2] */
type ContextType = typeof initialValue;

/**
 * [0] определение инициализационного значения,
 * на основе которого при помощи запроса типа [2]
 * будет получен его тип [1].
 */

Полученный тип необходимо будет указать в аннотации единственного параметра render callback при его определении.

typeScript
class ClassComponent extends Component {
    /**[0]                      [1] */
    renderCallback = (data: ContextType) => (
        <span>{data.message}</span>
    );


    render(){
        return (
                                    /**[2] */
            <Context.Consumer >{this.renderCallback}</Context.Consumer>
        );
    }
}

/**
 * При внешнем [2] определении render callback как поля класса [0]
 * в аннотации тип его единственного параметра указан тип данных [1]
 * предоставляемых контекстом. 
 * 
 */

Если данные предоставляемые контекстом принадлежать к более общему типу, то параметр render callback можно конкретизировать.

ts
/**[0] */
interface Message {
    message: string;
}

/**[0] */
interface Status {
    status: string;
}

      /**[1]             [2] */
type ContextType = Message & Status;


let initialValue = {
    status: ``,
    message: ``
};
const Context = createContext(initialValue);



/**
 * [0] объявление конкретных типов
 * определяющих тип пересечение [2]
 * на который ссылается прежний псевдоним [1].
 * 
 * Поскольку инициализационное значение в полной
 * мере соответствует предполагаемому типу, переменную
 * initialValue и универсальную функцию можно избавить от
 * явной и излишней конкретизации.
 */


 class ClassComponent extends Component {
                           /**[3] */
    renderCallback = (data: Message) => (
        <span>{data.message}</span>
    );


    render(){
        return (
            <Context.Consumer >{this.renderCallback}</Context.Consumer>
        );
    }
}

/**
 * [3] параметр render callback теперь ограничен типом
 * Message.
 */

Для получения данных распространяемых контекстом внутри тела функционального компонента, помимо варианта с Consumer, который ничем не отличается от рассмотренного в этой теме ранее, предусмотрен более предпочтительный способ предполагающий использование предопределенного хука useContext<T>(context).

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

ts
const FunctionComponent = () => {
    let {message, status} = useContext(Context);


    return (
        <span>{message}</span>
    );
}

const App = () => (
    <Context.Provider value={{status: `init`, message: `React Context!`}}>
        <FunctionComponent /> 
    </Context.Provider>
)

При попытке с помощью аргумента типа ограничить более общий тип данных возникнет ошибка, поскольку в действие включаются правила контравариантности параметров функции.

ts
const FunctionComponent = () => {
                              /**[0]      [1] */
    let {message} = useContext<Message>(Context); // Error


    return (
        <span>{message}</span>
    );
}

/**
 * При попке ограничить тип с помощью аргумента типа [0]
 * из-за контравариантности параметров функции возникнет ошибка [1]. 
 */