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

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

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

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

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

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

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

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

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

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

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

<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}
/>

4 ответов


Общий Подход

я бы создал 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'
  })`

  // ...and returns a new component wrapping the parameter component
  return hoistNonReactStatics(Private, WrappedComponent)
}

export default privateRoute

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

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

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

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

Редирект Логика

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

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

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

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

return hoistNonReactStatics(
  graphql(gql`
    query ...
  `)(Private),
  WrappedComponent
)

тогда вы можете изменить 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 = ({
  requireLogedIn = 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 params для путей, как мы сделали, чтобы позволить компоненту решить. Затем используйте эти значения в качестве 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'
  })`

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

export default privateRoute

я использую подъем-non-react-statics как полагают в официальная документация.


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

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

вам придется использовать ApolloClient без "react-graphql" HOC.
1. Получить экземпляр ApolloClient
2. Запрос огня
3. While 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


Я personnaly использовать для создания моих частных маршрутов, как это:

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 если пользователь зарегистрирован (зависит от того, как вы обрабатываете сеанс пользователя), вы можете создать каждый из ваших частных маршрутов.

затем can используйте его в коммутаторе:

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

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

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