Как предотвратить состояние гонки с помощью React Context API

React

В сервисной команде Stream мы с удовольствием работаем с различными клиентами и ежедневно решаем захватывающие задачи.

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

Мы недавно объединились с ребятами из Branch для реализации диплинков и функции навигации для наших клиентов.

Идея была проста: использовать панель управления Branch для создания диплинков с определенными полезными загрузками данных, а при нагрузке клиентского приложения, связанной с диплинками, получить доступ к данным для различных задач.

Звучит достаточно просто, пока вы не обнаруживаете, что нативные методы, на которых построен код Branch, не всегда работают синхронно с базой кода React Native/JavaScript.

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

Очень быстро мы поняли, что нативные методы Branch не подходят к жизненному циклу наших React Native компонентов. В то время как нативный код запускался немедленно, независимо от того, насколько глубоко было ветвление функции слушателя, загрузка данных была доступна только при начальной загрузке.

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

В наши дни существует множество способов хранения и обмена данными — от Redux до пробрасывания prop. После долгих обсуждений, мы остановились на React’s Context API.

Себастьян Маркбейдж, по сути крестный отец Hooks и Context, говорит, что Context лучше всего подходит для “низкочастотных, маловероятных обновлений” и “статических значений”.

Соответственно, однократное получение объекта пар “ключ-значение” диплинка идеально подходит для этой задачи.

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

Первый шаг в нашем примере — это создание React Context, который мы используем для хранения данных полезной нагрузки, полученных из диплинка Branch.

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

// создаем React context для управления глобальным состоянием
const BranchContext = createContext();

// получаем доступ к контекстному значению через компонент Consumer
export const BranchConsumer = BranchContext.Consumer;

// сохраняем данные и делимся ими с дочерними элементами через компонент provider 
const BranchProvider = ({ children }) => {
  const [contextData, setContextData] = useState({});
  const contextValue = { contextData, setContextData };

  return (
    <BranchContext.Provider value={contextValue}>
      {children}
    </BranchContext.Provider>
  );
};

export default BranchProvider;

В этом файле мы сделали следующее:

  • Создали контекст React Context, чтобы отслеживать данные диплинка.
  • Создали компонент consumer, который может передавать определенные значения, содержащиеся в контексте состояния, дочерним компонентам.
  • Создали компонент provider, который позволяет компоненту consumer оборачивать дочерние элементы, предоставляя им доступ к данным в качестве props.

Вы также заметите, что мы используем React’s Hooks API для локальных состояний, в особенности хук useState. Второе значение, возвращаемое функцией useState, дает нам возможность сбросить первое.

В нашем случае любой аргумент, переданный в функцию setContextData, перезапишет значение contextData. В основном мы запускаем старый основанный на классе метод setState с гораздо меньшим количеством строк кода.

Сейчас вы увидите, как этот сеттер становится удобным.

Добавляем контекстно-зависимый код в файл App.js.

import React, { useEffect } from "react";
import { View, Text } from "react-native";
import branch from "react-native-branch";

import BranchProvider, { BranchConsumer } from "./BranchContext";

let App = ({ setContextData }) => {
  // запускаем код в ответе useEffect при загрузке приложения
  useEffect(() => {
    // метод подписки branch слушает активность диплинка 
    branch.subscribe(({ error, params }) => {
      if (error) return;
      // если приложение загружается через диплинк, сохраняем его параметры в контексте 
      setContextData(params);
    });
  }, []);

  return (
    <View>
      <Text>Save Deeplink Content on App Load!</Text>
    </View>
  );
};

App = props => (
  // передаем значение, хранящееся в контексте, дочернему элементу 
  <BranchProvider>
    {/* деструктурируем значения контекста и передаем как props внутрь приложения */}
    <BranchConsumer>
      {({ setContextData }) => (
        <App {...props} setContextData={setContextData} />
      )}
    </BranchConsumer>
  </BranchProvider>
);

export default App;

В этом файле мы сделали следующее:

  • Завернули приложение в компонент provider для доступа контекста.
  • Завернули приложение в компонент consumer и передали функцию-сеттер контекста через props.
  • Активировали слушатель диплинка Branch при загрузке.
  • Использовали функцию-сеттер контекста для хранения любых параметров диплинка, выбранных слушателем Branch.

Когда приложение загружается, запускается функция вызова в хуке useEffect и активируется слушатель Branch. Надеюсь, теперь вы видите пользу в использовании useState в нашем случае.

Если слушатель Branch принимает диплинк, функция-сеттер useState, которую мы передали внутрь приложения через consumer контекста, сохраняет данные диплинка в переменную contextData внутри BranchContext.

Как только мы получим эти данные, любой компонент, который мы завернем в компонент BranchConsumer, сможет получить доступ к contextData через props.

Последний файл в приложении показывает, как получить доступ к данным, которые сохранены в контексте:

import React from "react";
import { View, Text } from "react-native";

import { BranchConsumer } from "./BranchContext";

let DataConsumer = ({ deeplinkContent }) => {
  /* 
    в этот момент все пары ключ-значение, встроенные в диплинк, могут быть удалены из объекта `deeplinkContent` для использования внутри приложения
  */

  return (
    <View>
      <Text>Access Deeplink Content in this Component!</Text>
    </View>
  );
};

DataConsumer = props => (
  // передаем в качестве props данные диплинка, сохраненные при загрузке приложения в DataConsumer 
  <BranchConsumer>
    {({ contextData }) => (
      <DataConsumer {...props} deeplinkContent={contextData} />
    )}
  </BranchConsumer>
);

export default DataConsumer;

В этом файле мы сделали:

  • Завернули компонент в consumer и передали любые сохраненные данные контекста в качестве props.
  • Позволили компоненту получить доступ к любым парам ключ-значение, встроенным в диплинк Branch.

Обернув функциональный компонент React в компонент consumer, DataConsumer теперь может получать доступ к тем же объектам данных, которые слушатель Branch получает при загрузке приложения.

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

Но потенциал использования бесконечен, так как любые данные остаются доступны, если хранить их внутри объекта JavaScript.

Context API спасает нас от неаутентифицированных кликов по диплинкам, особенно при выходе из системы или, когда потенциальные пользователи кликают на ссылки Branch. Когда пользователь вошел в систему, немедленное прочтение диплинка и навигация по содержимому имеют смысл.

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

Хранение данных диплинков внутри BranchContext позволяет нам провести пользователя через стандартный процесс аутентификации, и после успешного входа в систему, перейти к содержимому, указанному в диплинке Branch.

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

Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming