Как использовать 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}
/>

person AnApprentice    schedule 08.02.2018    source источник


Ответы (5)


Основной подход

Я бы создал 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);

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

<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


Я использую hoist-non-react-statics, как предложено в официальную документацию.

person Luke    schedule 15.02.2018
comment
Привет, Люк, у тебя тоже будет время ответить на мой вопрос? stackoverflow .com / questions / 60475091 /. - person x89; 01.03.2020

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

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

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

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

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

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

person Dyo    schedule 12.02.2018
comment
Привет, Дайо, у тебя тоже будет время ответить на мой вопрос? stackoverflow .com / questions / 60475091 /. - person x89; 01.03.2020

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

<Route path="/onboarding" render={renderProps=>
   <CheckAuthorization authorized={OnBoardingPage} renderProps={renderProps} />
}/>
person Dennie de Lange    schedule 08.02.2018

Вам придется использовать ApolloClient без HOC 'react-graphql'.
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

person Chromonav    schedule 11.02.2018
comment
Здравствуйте, у вас есть время, чтобы ответить на мой вопрос? stackoverflow .com / questions / 60475091 /. - person x89; 01.03.2020

Если вы используете клиент apollo response, вы также можете импортировать Query @apollo/components и использовать его в своем частном маршруте:

    <Query query={fetchUserInfoQuery(moreUserInfo)}>
      {({ loading, error, data: userInfo = {} }: any) => {
        const isNotAuthenticated = !loading && (isEmpty(userInfo) || !userInfo.whoAmI);

        if (isNotAuthenticated || error) {
          return <Redirect to={RoutesPaths.Login} />;
        }
        const { whoAmI } = userInfo;

        return <Component user={whoAmI} {...renderProps} />;
      }}
    </Query>

где isEmpty просто проверяет, пуст ли данный объект:

const isEmpty = (object: any) => object && Object.keys(object).length === 0
person Daler    schedule 23.12.2019
comment
Здравствуйте, у вас есть время, чтобы ответить на мой вопрос? stackoverflow .com / questions / 60475091 /. - person x89; 01.03.2020