Загрузка приложения с несколькими глобальными зависимостями может быть сложной задачей. Интернационализация, аутентификация, библиотека компонентов, тема, маршрутизация, управление состоянием, управление сеансом, границы ошибок и т. Д. - вот лишь некоторые из частей, которые вам нужно будет предоставить в своем приложении React.

В этой статье я попытаюсь представить схему четкого разделения проблем. Здесь нет оригинальных идей. Суть его взята из блогов Эрика Эллиотта и Кента Доддса.

Конечный результат будет примерно таким:

export const unAuthenticatedPage = compose(
  withI18n,
  withStrictMode,
  withRouter,
  withAuthentication,
  withLoginShell
);
export const authenticatedPage = compose(
  withI18n,
  withStrictMode,
  withRouter,
  withRedux,
  withAuthentication,
  withAxiosHeaders,
  withAppShell,
  withErrorBoundary
);
export const App = () => {
  const loginInfo = getUser();
  return isUserAuthenticated(loginInfo.username)
    ? authenticatedPage(() => <AppContainer />)
    : unAuthenticatedPage(() => <LoginContainer />);
};

Я хочу продемонстрировать, насколько просто и красиво это выражено. В приложении два персонажа. Один аутентифицирован, а другой нет. Оба требуют разной комбинации глобальной начальной загрузки. Секрет в том, как вы пишете свои HOC-оболочки.

Обертки HOC

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

withRedux

const store = createStore(combineReducers(allAsyncReducers));
const withRedux = (Component) => (props) => (
  <Provider store={store}>
    <Component {...props} />
  </Provider>
);

withi18n

Настроить i18n

import { initReactI18next } from "react-i18next";
import translations from "../locales";
const withI18n = (Component) => (props) => {
  i18n
    .use(LanguageDetector)
    .use(initReactI18next)
    .init({
      resources: translations
    });
  return <Component {...props} />;
};

withRouter

Настроить response-router

const withRouter = (Component) => (props) => (
  <Router basename={"/atlas"}>
    <Component {...props} />
  </Router>
);

withStrictMode

export const withStrictMode = (Component) => (props) => (
  <StrictMode>
    <Component {...props} />
  </StrictMode>
);

withAppShell

Общие элементы пользовательского интерфейса, которые охватывают функции приложения

export const withAppShell = (Component) => (props) => (
  <div>
    <Header>
      <Navigation />
    </Header>
    <AppContent>
       <Component {...props} />
    </AppContent>
    <Footer />
  </div>
);

withErrorBoundary

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

export const withErrorBoundary = (Component) => (props) => (
  <ErrorBoundary>
    <Component {...props} />
  </ErrorBoundary>
);

Аутентификация

Например.

export const withAuthentication = (Component) => (props) => {
  // reducer manages the authenticated user info
  const [state, dispatch] = useReducer(reducer, initialState);
  const { setUser } = actions(dispatch);
  const context = {
    state,
    actions: { setUser, setToken }
  };
  React.useEffect(() => {
   // get use authentication details.
   if (loginInfo?.username) {
      setUser(loginInfo.username);
    } else {
      logout();
    }
  }, []);
return (
    <>
      <AuthenticationContext.Provider value={loginInfo.username}>
        <Component {...props} />
      </AuthenticationContext.Provider>
    </>
  );
};

Собираем все вместе

Если вы заметили, все эти оболочки HOC имеют одинаковую сигнатуру функции:

withXXX = (Component) => (props) => { ... }

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

import { compose } from "ramda";
export const unAuthenticatedPage = compose(
  withI18n,
  withStrictMode,
  withRouter,
  withAuthentication,
  withLoginShell
);
export const authenticatedPage = compose(
  withI18n,
  withStrictMode,
  withRouter,
  withRedux,
  withAuthentication,
  withAxiosHeaders,
  withAppShell,
  withErrorBoundary
);

Теперь настроить приложение стало очень просто:

export const App = () => {
  const loginInfo = getUser();
  return userIsAuthenticated(loginInfo.username)
    ? authenticatedPage(() => <AppContainer />)
    : unAuthenticatedPage(() => <LoginContainer />);
};

Этот шаблон обеспечивает простой способ объединения функций вашего приложения в определенном контексте. Вы можете расширить некоторые из этих контекстов, предоставив настраиваемые хуки useContext (). Допустим, вы хотите использовать зарегистрированное имя пользователя в других частях приложения. Для таких библиотек, как Redux, контекст уже предоставлен, и крючков, предоставляемых библиотекой, достаточно.

export const useAuthentication = () => {
  return React.useContext(AuthenticationContext);
};

Так просто. Хуки в библиотеках, таких как Redux, будут работать без труда после того, как вы включите приложение в его поставщик контекста.

Резюме

В этой статье представлен простой, но мощный паттерн, который может загружать ReactApp с очень изолированными, слабосвязанными реализациями. Расширять их или добавлять новые становится очень легко. Все, что вам нужно сделать, это создать HOC и скомпоновать его, как показано выше.

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