Аутентификация с помощью React Context API и компонентов более высокого порядка

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

Вы можете ознакомиться с работающим приложением здесь, на codesandbox.io, вам понадобится приложение firebase и репозиторий github для кода

Недавно я решил НЕ использовать Redux для стороннего проекта, над которым я работал. Моя первая игра React-no-Redux. Это было минимальное приложение, и мне, кстати, не нужно было сложное управление состоянием, с которым так хорошо справляется Redux.

В этой статье я поделюсь, как я справился с аутентификацией, объединив силу следующего:

  • Реагировать на состояние
  • Контекстный API реакции
  • Реактивный маршрутизатор
  • Компоненты высшего порядка

Давайте начнем со списка того, чего мы хотим достичь

  • Компонент Authentication Provider прослушивает изменения статуса аутентификации от серверной службы, которая фактически обрабатывает аутентификацию. здесь нет ничего страшного
  • Обновлять приложение при изменении статуса аутентификации
  • Сохранить текущее состояние аутентификации
  • Защитите части приложения от несанкционированного просмотра или обновления

Давайте начнем

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

Во-первых, давайте создадим Context с помощью этих строк кода.

import React, { Component } from 'react'
const defaultValue = {} // for orphan Consumers
const AuthContext = React.createContext(defaultValue)

Легкий! Вот и все, AuthContext теперь содержит два мощных свойства; Providerи Consumer. Думайте о Provider как о родителях, содержащих значения, а Consumer. как ребенок (дети), наследующий и потребляющий указанные значения. Имейте в виду, что каждый вызов React.createContext возвращает набор Provider и Consumer, которые будут работать только друг с другом. Например, <AuthContext.Consumer/> не может прочитать значение, скажем, из <ArticlesContext.Provider />.

Теперь вот компонент поставщика аутентификации, который прослушивает изменения в статусе аутентификации пользователя из бэкэнда (в моем случае firebase) и обновляет приложение по обратной связи.

export class AuthProvider extends Component {  
  constructor () {    
    super()    
    this.state = {      
      waiting: true    
    }  
  }   
  componentDidMount () {    
    firebaseAuth.onAuthStateChanged(user => {
      this.setState(toKeyVal('user', user))
      this.setState(toKeyVal('waiting', false))
    })
  }
  render () {    
    const { children } = this.props    
    const { user, waiting } = this.state
    const { Provider } = AuthContext  
    if (waiting) {      
      return 'Loading....'   
    }     
    return <Provider value={user}>{children}</Provider>  
  }
}

Краткое объяснение; Как только этот компонент, AuthProvider, смонтирован, он начинает прослушивать серверную часть на предмет изменений в статусе пользователя и обновляет свое состояние с ответом. user изначально равно undefined и останется таковым до тех пор, пока бэкенд не сообщит, что есть аутентифицированный пользователь. Не говоря уже о функции toKeyVal, это всего лишь небольшая вспомогательная функция, которую мне нравится использовать с React setState.

Затем мы используем значение `user`, хранящееся у провайдера, с помощью React Higher Order Component.

export const withAuth = authoriserFunc => {
  const { Consumer } = AuthContext;
  return ChildComponent => props => (
    <Consumer>
      {user => {
        if (!(user && user.uid)) {
          console.warn("redirecting: no user object found");
          return <Redirect push to="/login" />;
        }
        const authorisedUserData = authoriserFunc
          ? authoriserFunc(user) : { ...user };
        const developedProps = { ...authorisedUserData, ...props };
        return <ChildComponent {...developedProps} />;
      }}
    </Consumer>
  );
};

Когда Consumer появляется как потомок Provider, значение Provider автоматически становится доступным для Consumer . Таким образом, функция withAuth HOC здесь принимает другую функцию authoriserFunc в качестве аргумента,

Сначала мы проверяем, определен ли user, иначе перенаправляем на Login. Затем мы запускаем authoriserFunc для значения user, чтобы выполнить проверку авторизации и вернуть объект. Функция withAuth возвращает другую функцию, которая принимает целевой компонент ChildComponent в качестве аргумента и возвращает компонент AuthConsumer с одним дочерним элементом target(ChildComponent) с авторизованными пользовательскими данными, распространенными в нем в качестве реквизита.

И чтобы выровнять его с потоком нашего приложения, мы добавляем это в файл, где мы объявляем наши (верхнего уровня) маршруты, index.js.

import React, { Fragment } from "react";
import ReactDOM from "react-dom";
import { 
  BrowserRouter as Router,
  Route,
  Link,
  Switch
} from "react-router-dom";
import Login from "./LoginContainer";
import Moderators from "./ModeratorsOnly";
import Writer from "./Writers";
import { AuthProvider, logout, withAuth } from "./auth";
.......
<Router>
  <Switch>
    <Route path="/login" component={Login} />
    <AuthProvider>
      <Route path="/moderators" component={Moderators} />
      <Route path="/writers" component={Writer} />
      <Route render={withAuth()(() => (
        <Fragment>
          <p><Link to="/moderators">Moderators</Link></p>
          <Link to="/writers">Writers</Link>
          <p><button onClick={() => logout()}>Logout</button></p>
        </Fragment>)
      )}/>
    </AuthProvider>
    <Route path="/" render={() => <Link to="/login">Login</Link>} />
  </Switch>
</Router>

А потом в PrivateComponent заворачиваем в withAuth HOC

const PrivateComponent = () => <Fragment>private content</Fragment>
export const myPrivateComponent = withAuth()(PrivateComponent)

Мне нравится тот факт, что это позволяет авторизоваться на основе компонентов. До сих пор я использовал шаблон, предложенный в документации по реактивному маршрутизатору, где вы определили компонент и PrivateComponent сделали его рендеринг (верхний уровень) закрытым <Route>. Это определенно заходит очень далеко, но в более крупном проекте с множеством требований к безопасности компонентов один компонент, который выполняет все проверки, рано или поздно сделает слишком много и может начать создавать собственные проблемы. Однако этот шаблон дает авторам компонентов свободу определять, как этот компонент ведет себя при авторизации.

Например, мы можем легко ограничить права редактирования для определенного типа пользователей.

import React, { Fragment } from "react";
import { withAuth } from "./auth";
const Writer = ({ error }) => {
  const view = [];
  if (error) {
    view.push(<div key="1-irwr">Failed to load: {error}</div>);
  } else {
    view.push(<div key="4-fkjjs">Welcome Writer you</div>);
  }
  return <Fragment>{view}</Fragment>;
};
/**
* DANGERFOOL!!! Do not try this at home or anywhere else
* firebase handles Access Control with idTokens and claims
* read about it here https://firebase.google.com/docs/auth/admin/custom-claims
* whatever you do here or in any clientside security should be for UX purposes
* only and NEVER as the actual means to secure your app
*/
const authReducer = user => {
  const type = user.email.split("@")[0];
  if (!(type === "writer" || type === "moderator")) {
    return { error: "Only moderators and writers can view this page"
  };
  return { ...user };
};
export default withAuth(authReducer)(Writer);

Поиграй с ним @codesandbox.

Контекст с HOC может быть очень мощным инструментом в вашем наборе инструментов Reactjs. Далее я хочу научиться добавлять события в микс так, как это делают некоторые библиотеки, такие как redux, чтобы обеспечить надежность, которую они имеют.

Я все еще учусь и надеюсь когда-нибудь стать лучше в этих вещах, но если я допустил ошибки, пожалуйста, укажите на них в комментариях.