Вступление
Привет, дорогой читатель!
Некоторое время (около года) назад столкнулся с необходимостью условной отрисовки компонентов в ReactJS в зависимости от текущих прав пользователя. Первым делом начал искать готовые решения и «лучшие практики». Статья "Role based authorization in React" произвела больше всего впечатления своим использованием Higher-Order Components (HOC). Но, к сожалению, решения, которое меня удовлетворяет, не нашел.
Видимо, все-таки что-то упустил...
… или не знал о существовании контекстов. На момент написания статьи наткнулся на замечательный ответ в stackoverflow. У меня в итоге получилось сильно похожее решение.
В то время был немного знаком с «react-redux-connect» (npm-модуль), и меня сильно зацепил подход с декорированием, который используется в функции connect. Подробный разбор устройства connect можно найти тут.
Описание решения
Для начала надо определить какая минимальная информация нужна для принятия решения об отрисовке компонента. Очевидно, для отрисовки необходимо выполнение некоторого условия (как вариант наличие какого-то права — например, право добавления нового пользователя). Назовем это условие требованием (или requirement на английском). Понять, было ли требование удовлетворено, можем на основе набора текущих прав пользователя — credentials. То есть достаточно определить функцию:
function isSatisfied(requirement, credentials) {
if (...) {
return false;
}
return true;
}
Теперь мы более или менее определились с условием отрисовки. А как это использовать?
1. Можем использовать подход в лоб:
const requirement = {...};
class App extends Component {
render() {
const {credentials} = this.props;
return isSatisfied(requirement, credentials) && <TargetComponent>;
}
}
2. Можем пойти чуть дальше, и обернуть целевой компонент в другой, который и будет делать проверку выполнения требования:
const requirement = {...};
class ProtectedTargetComponent extends Component {
render() {
const {credentials} = this.props;
return (
isSatisfied(requirement, credentials)
? <TargetComponent {...this.props}>
{this.props.children}
</TargetComponent>
: null
);
}
}
class App extends Component {
render() {
const {credentials} = this.props;
return <ProtectedTargetComponent/>;
}
}
Вручную писать обертку для каждого целевого компонента довольно муторно. Как это можем упростить?
3. Можем прибегнуть к механизму HOC (по аналогии с connect из «react-redux-connect»):
function protect(requirement, WrappedComponent) {
return class extends Component {
render() {
const { credentials } = this.props;
return (
isSatisfied(requirement, credentials)
? <WrappedComponent {...this.props}>
{this.props.children}
</WrappedComponent>
: null
);
}
}
}
...
const requireAdmin = {...};
const AdminButton = protect(requireAdmin, Button);
...
class App extends Component {
render() {
const {credentials} = this.props;
return (
...
<AdminButton credentials={credentials}>
Add user
</AdminButton>
...
);
}
}
Уже лучше, но всё еще убого — нужно руками пробрасывать credentials через всё дерево компонентов. Что с этим можно сделать? Логично предположить, что credentials текущего пользователя — это глобальный объект для всего приложения. Тогда на помощь снова приходит «react-redux-connect». Почитав статью об устройстве этого модуля, обнаруживаем, что в нём используются некие контексты ReactJS.
4. С использованием механизма контекстов получаем окончательный подход:
const { Provider, Consumer } = React.createContext();
function protect(requirement, WrappedComponent) {
return class extends Component {
render() {
return (
<Consumer>
{ credentials => isSatisfied(requirement, credentials)
? <WrappedComponent {...this.props}>
{this.props.children}
</WrappedComponent>
: null
}
</Consumer>
);
}
}
}
...
const requireAdmin = {...};
const AdminButton = protect(requireAdmin, Button);
...
class App extends Component {
render() {
const { credentials } = this.props;
return (
<Provider value={credentials}>
...
<AdminButton>
Add user
</AdminButton>
...
</Provider>
);
}
}
Послесловие
Это был краткий экскурс в саму идею. На базе этой идеи был реализован модуль (github, npm), который предоставляет более интересные возможности и его проще встроить (см. README.md в гитхабе и демо с использованием модуля).
Только мне почему-то не удалось завести созданный npm пакет в демо, поэтому пришлось туда вставлять сам код модуля. Но модуль, установленный через npm install react-rbac-guard, локально работает (Chrome 69.0.3497.100). Подозреваю, что проблема в способе сборки — я просто скопировал файлы package.json и webpack.config.prod.js из модуля с аналогичным предназначением.
Так как я не являюсь фронтенд разработчиком, ещё много чего недоделанного (отсутствие тестов, неработоспособность в https://codesandbox.io и, возможно, другие упущенные моменты). Поэтому, если будут замечания, предложения или пулл-реквесты, то добро пожаловать!
P.P.S.: Все замечания по поводу правописания, в том числе в README.md, просьба присылать в личные сообщения или в виде пулл-реквеста.
Комментарии (5)
alexesDev
30.10.2018 09:44Не стоит оформлять HOC вот так
function protect(requirement, WrappedComponent)
на порядок лучше
const protect = requirement => WrappedComponet => props => { ... }
вашу версию нельзя компоновать через compose
import { compose } from 'redux'; import { connect } from 'react-redux'; const enhance = compose( protect({ ... }), connect(...), ); export default enhance(App);
Nurzhan69 Автор
30.10.2018 10:45Спасибо, теперь понимаю, почему именно такой интерфейс у connect.
Кстати, в реализации protect оформлен нужным образом.
allexx
function isSatisfied(requirement, credentials) {
if (...) {
return false;
}
return true;
}
Куда безопаснее сделать наоборот
Nurzhan69 Автор
Наверное, это всё же к предпочтительности принципа «запрещено всё, что не разрешено» перед принципом «разрешено всё, что не запрещено»?