Как использовать Apollo Client + React Router для реализации частных маршрутов и перенаправления на основе статуса пользователя?

Я использую React Router 4 для маршрутизации и Apollo Client для получения данных & кэширование. Мне нужно реализовать решение PrivateRoute и перенаправления на основе следующих критериев:

  1. Страницы, которые пользователю разрешено просматривать, основаны на их статусе пользователя, который можно получить с сервера или прочитать из кэша. Статус пользователя - это, по сути, набор флагов, которые мы используем, чтобы понять, где находится пользователь в нашей воронке. Флаги примеров: isLoggedIn, isOnboarded, isWaitlisted и т.д.

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

  3. Перенаправление также должно быть динамическим. Например, скажем, вы пытаетесь просмотреть свой профиль пользователя, прежде чем вы isLoggedIn. Затем нам нужно перенаправить вас на страницу входа. Однако, если вы isLoggedIn, но не isOnboarded, мы все равно не хотим, чтобы вы видели свой профиль. Поэтому мы хотим перенаправить вас на страницу регистрации.

  4. Все это должно происходить на уровне маршрута. Сами страницы должны быть не осведомлены об этих разрешениях & переназначения.

В заключение нам нужна библиотека, которая, учитывая данные о состоянии пользователя, может

  • вычислить, может ли пользователь находиться на определенной странице
  • вычислить, куда они должны быть перенаправлены динамически
  • сделайте это перед отображением любой страницы
  • сделать это на уровне маршрута

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

Вот мой нынешний подход. Это не работает, потому что данные, которые нужны getRedirectPath, находятся в OnboardingPage component.

Кроме того, я не могу обернуть PrivateRoute HOC, который мог бы внедрить реквизиты, необходимые для вычисления пути перенаправления, потому что это не позволило бы мне использовать его как дочерний компонент компонента Switch React Router, поскольку он перестает быть маршрутом.

<PrivateRoute
  exact
  path="/onboarding"
  isRender={(props) => {
    return props.userStatus.isLoggedIn && props.userStatus.isWaitlistApproved;
  }}
  getRedirectPath={(props) => {
    if (!props.userStatus.isLoggedIn) return '/login';
    if (!props.userStatus.isWaitlistApproved) return '/waitlist';
  }}
  component={OnboardingPage}
/>

Ответ 1

Общий подход

Я бы создал HOC для обработки этой логики для всех ваших страниц.

// privateRoute is a function...
const privateRoute = ({
  // ...that takes optional boolean parameters...
  requireLoggedIn = false,
  requireOnboarded = false,
  requireWaitlisted = false
// ...and returns a function that takes a component...
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      // redirect logic
    }

    render() {
      if (
        (requireLoggedIn && /* user isn't logged in */) ||
        (requireOnboarded && /* user isn't onboarded */) ||
        (requireWaitlisted && /* user isn't waitlisted */) 
      ) {
        return null
      }

      return (
        <WrappedComponent {...this.props} />
      )
    }
  }

  Private.displayName = 'Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })'

  hoistNonReactStatics(Private, WrappedComponent)

  // ...and returns a new component wrapping the parameter component
  return Private
}

export default privateRoute

Тогда вам нужно всего лишь изменить способ экспорта своих маршрутов:

export default privateRoute({ requireLoggedIn: true })(MyRoute);

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

<Route path="/" component={MyPrivateRoute} />

Логика перенаправления

Как вы установите эту часть, зависит от нескольких факторов:

  1. Как вы определяете, вошел ли пользователь в систему, включен ли он, находится ли в списке ожидания и т.д.
  2. На какой компонент вы хотите отвечать, куда перенаправить.

Обработка статуса пользователя

Поскольку вы используете Apollo, вы, вероятно, просто захотите использовать graphql, чтобы получить эти данные в вашем HOC:

return graphql(gql'
  query ...
')(Private)

Затем вы можете изменить компонент Private, чтобы получить эти реквизиты:

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      }
    } = this.props

    if (requireLoggedIn && !isLoggedIn) {
      // redirect somewhere
    } else if (requireOnboarded && !isOnboarded) {
      // redirect somewhere else
    } else if (requireWaitlisted && !isWaitlisted) {
      // redirect to yet another location
    }
  }

  render() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      ...passThroughProps
    } = this.props

    if (
      (requireLoggedIn && !isLoggedIn) ||
      (requireOnboarded && !isOnboarded) ||
      (requireWaitlisted && !isWaitlisted) 
    ) {
      return null
    }

    return (
      <WrappedComponent {...passThroughProps} />
    )
  }
}

Куда перенаправить

Есть несколько разных мест, где вы можете справиться с этим.

Простой способ: маршруты статичны

Если пользователь не вошел в систему, вы всегда хотите направить на /login?return=${currentRoute}.

В этом случае вы можете просто жестко закодировать эти маршруты в componentDidMount. Готово.

Компонент отвечает

Если вы хотите, чтобы компонент MyRoute определял путь, вы можете просто добавить некоторые дополнительные параметры в функцию privateRoute, а затем передать их при экспорте MyRoute.

const privateRoute = ({
  requireLoggedIn = false,
  pathIfNotLoggedIn = '/a/sensible/default',
  // ...
}) // ...

Затем, если вы хотите переопределить путь по умолчанию, вы измените свой экспорт на:

export default privateRoute({ 
  requireLoggedIn: true, 
  pathIfNotLoggedIn: '/a/specific/page'
})(MyRoute)

Маршрут ответственен

Если вы хотите иметь возможность передавать путь от маршрутизации, вы хотите получать реквизиты для них в Private

class Private extends Component {
  componentDidMount() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      pathIfNotLoggedIn,
      pathIfNotOnboarded,
      pathIfNotWaitlisted
    } = this.props

    if (requireLoggedIn && !isLoggedIn) {
      // redirect to 'pathIfNotLoggedIn'
    } else if (requireOnboarded && !isOnboarded) {
      // redirect to 'pathIfNotOnboarded'
    } else if (requireWaitlisted && !isWaitlisted) {
      // redirect to 'pathIfNotWaitlisted'
    }
  }

  render() {
    const {
      userStatus: {
        isLoggedIn,
        isOnboarded,
        isWaitlisted
      },
      // we don't care about these for rendering, but we don't want to pass them to WrappedComponent
      pathIfNotLoggedIn,
      pathIfNotOnboarded,
      pathIfNotWaitlisted,
      ...passThroughProps
    } = this.props

    if (
      (requireLoggedIn && !isLoggedIn) ||
      (requireOnboarded && !isOnboarded) ||
      (requireWaitlisted && !isWaitlisted) 
    ) {
      return null
    }

    return (
      <WrappedComponent {...passThroughProps} />
    )
  }
}

Private.propTypes = {
  pathIfNotLoggedIn: PropTypes.string
}

Private.defaultProps = {
  pathIfNotLoggedIn: '/a/sensible/default'
}

Затем ваш маршрут можно переписать на:

<Route path="/" render={props => <MyPrivateComponent {...props} pathIfNotLoggedIn="/a/specific/path" />} />

Объедините варианты 2 & 3

(Это подход, который мне нравится использовать)

Вы также можете позволить компоненту и маршруту выбирать, кто несет ответственность. Вам просто нужно добавить параметры privateRoute для путей, как мы делали, чтобы позволить компоненту решать. Затем используйте эти значения в качестве defaultProps, как мы делали, когда был ответственен маршрут.

Это дает вам гибкость при принятии решений. Просто отметьте, что прохождение маршрутов в качестве реквизита будет иметь приоритет над прохождением из компонента в HOC.

Теперь все вместе

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

const privateRoute = ({
  requireLoggedIn = false,
  requireOnboarded = false,
  requireWaitlisted = false,
  pathIfNotLoggedIn = '/login',
  pathIfNotOnboarded = '/onboarding',
  pathIfNotWaitlisted = '/waitlist'
} = {}) => WrappedComponent => {
  class Private extends Component {
    componentDidMount() {
      const {
        userStatus: {
          isLoggedIn,
          isOnboarded,
          isWaitlisted
        },
        pathIfNotLoggedIn,
        pathIfNotOnboarded,
        pathIfNotWaitlisted
      } = this.props

      if (requireLoggedIn && !isLoggedIn) {
        // redirect to 'pathIfNotLoggedIn'
      } else if (requireOnboarded && !isOnboarded) {
        // redirect to 'pathIfNotOnboarded'
      } else if (requireWaitlisted && !isWaitlisted) {
        // redirect to 'pathIfNotWaitlisted'
      }
    }

    render() {
      const {
        userStatus: {
          isLoggedIn,
          isOnboarded,
          isWaitlisted
        },
        pathIfNotLoggedIn,
        pathIfNotOnboarded,
        pathIfNotWaitlisted,
        ...passThroughProps
      } = this.props

      if (
        (requireLoggedIn && !isLoggedIn) ||
        (requireOnboarded && !isOnboarded) ||
        (requireWaitlisted && !isWaitlisted) 
      ) {
        return null
      }
    
      return (
        <WrappedComponent {...passThroughProps} />
      )
    }
  }

  Private.propTypes = {
    pathIfNotLoggedIn: PropTypes.string,
    pathIfNotOnboarded: PropTypes.string,
    pathIfNotWaitlisted: PropTypes.string
  }

  Private.defaultProps = {
    pathIfNotLoggedIn,
    pathIfNotOnboarded,
    pathIfNotWaitlisted
  }
  
  Private.displayName = 'Private(${
    WrappedComponent.displayName ||
    WrappedComponent.name ||
    'Component'
  })'

  hoistNonReactStatics(Private, WrappedComponent)
  
  return graphql(gql'
    query ...
  ')(Private)
}

export default privateRoute

Ответ 2

Думаю, вам нужно немного поправить свою логику. Что-то вроде:

<Route path="/onboarding" render={renderProps=>
   <CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>

Ответ 3

Я лично использую, чтобы строить свои частные маршруты следующим образом:

const renderMergedProps = (component, ...rest) => {
  const finalProps = Object.assign({}, ...rest);
  return React.createElement(component, finalProps);
};

const PrivateRoute = ({
  component, redirectTo, path, ...rest
}) => (
  <Route
    {...rest}
    render={routeProps =>
      (loggedIn() ? (
        renderMergedProps(component, routeProps, rest)
      ) : (
        <Redirect to={redirectTo} from={path} />
      ))
    }
  />
);

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

Затем вы можете использовать его в Switch:

<Switch>
    <Route path="/login" name="Login" component={Login} />
    <PrivateRoute
       path="/"
       name="Home"
       component={App}
       redirectTo="/login"
     />
</Switch>

Все подпрограммы из этого PrivateRoute сначала должны будут проверить, зарегистрирован ли пользователь.

Последний шаг - вложить маршруты в соответствии с их требуемым статусом.

Ответ 4

Вам нужно будет использовать ApolloClient без "реакции-graphql" HOC.
 1. Получить экземпляр ApolloClient
 2. Запрос огня
 3. В то время как Query возвращает загрузку данных.

 4. Проверьте и авторизуйте маршрут на основе данных.
 5. Возврат Соответствующий компонент или перенаправление.

Это можно сделать следующим образом:

import Loadable from 'react-loadable'
import client from '...your ApolloClient instance...'

const queryPromise = client.query({
        query: Storequery,
        variables: {
            name: context.params.sellername
        }
    })
const CheckedComponent = Loadable({
  loading: LoadingComponent,
  loader: () => new Promise((resolve)=>{
       queryPromise.then(response=>{
         /*
           check response data and resolve appropriate component.
           if matching error return redirect. */
           if(response.data.userStatus.isLoggedIn){
            resolve(ComponentToBeRendered)
           }else{
             resolve(<Redirect to={somePath}/>)
           }
       })
   }),
}) 
<Route path="/onboarding" component={CheckedComponent} />

Связанная ссылка API: https://www.apollographql.com/docs/react/reference/index.html