Всем привет! В этом учебнике я покажу вам как обрабатывать авторизацию в React-приложении используя Redux, react-router 4.x с react-router-redux 5.x и redux-saga.
Вам нужно будет установить последнюю версию react-router-redux:
npm install --save react-router-redux@next
npm install --save history
Инициализация
Во-первых, нам нужно создать роутер, сага-мидлевар, стор и запустить сагу.
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import createHistory from 'history/createBrowserHistory';
import createSagaMiddleware from 'redux-saga';
import { Provider } from 'react-redux';
import { routerMiddleware } from 'react-router-redux';
import { applyMiddleware, createStore } from 'redux';
import App from './App';
import Saga from './saga';
import reducer from './reducer';
// создаем мидлевары
const history = createHistory();
const sagaMiddleware = createSagaMiddleware();
const middleware = applyMiddleware(
routerMiddleware(history),
sagaMiddleware
);
// создаем стор
const store = createStore(reducer, middleware);
// запуск сага-мидлевара
sagaMiddleware.run(Saga);
ReactDOM.render(
<Provider store={store}>
<App history={history} />
</Provider>,
document.getElementById('root')
);
Редьюсер
Состояние приложения будет состоять из двух частей: одно для auth и одно для роутера. Редьюсер auth будет обрабатывать события аутентификации.
// reducer.js
import { combineReducers } from 'redux'
import { routerReducer } from 'react-router-redux';
export const AUTH_REQUEST = 'AUTH_REQUEST';
export const AUTH_SUCCESS = 'AUTH_SUCCESS';
export const AUTH_FAILURE = 'AUTH_FAILURE';
export const authorize = (login, password) => ({
type: AUTH_REQUEST,
payload: { login, password }
});
const initialState = {
token: localStorage.getItem('token'),
error: null
};
const authReducer = (state = initialState, { type, payload }) => {
switch (type) {
case AUTH_SUCCESS: {
return { ...state, token: payload };
}
case AUTH_FAILURE: {
return { ...state, error: payload }
}
default:
return state;
}
};
const reducer = combineReducers({
auth: authReducer,
router: routerReducer
});
export default reducer;
Сага
Сага будет следить за экшеном AUTH_REQUEST action и обрабатывать логику аутентификации.
// saga.js
import { call, put, takeLatest } from 'redux-saga/effects';
import { AUTH_REQUEST, AUTH_SUCCESS, AUTH_FAILURE } from './reducer';
const fetchJSON = (url, options = {}) =>
new Promise((resolve, reject) => {
return fetch(url, options)
.then(response => (response.status !== 200 ? reject(response) : response))
.then(response => response.json())
.then(response => resolve(response))
.catch(error => reject(error));
});
function* authorize({ payload: { login, password } }) {
const options = {
body: JSON.stringify({ login, password }),
method: 'POST',
headers: { 'Content-Type': 'application/json' }
};
try {
const { token } = yield call(fetchJSON, '/login', options);
yield put({ type: AUTH_SUCCESS, payload: token });
localStorage.setItem('token', token);
} catch (error) {
let message;
switch (error.status) {
case 500: message = 'Internal Server Error'; break;
case 401: message = 'Invalid credentials'; break;
default: message = 'Something went wrong';
}
yield put({ type: AUTH_FAILURE, payload: message });
localStorage.removeItem('token');
}
}
function* Saga() {
yield takeLatest(AUTH_REQUEST, authorize);
}
export default Saga;
Роутер
Мы используем роутер с двумя маршрутами, один для компонента Login и один для компонента Main.
//App.js
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import { ConnectedRouter } from 'react-router-redux';
import Main from './Main';
import Login from './Login';
const App = props => {
const { history } = props;
return (
<ConnectedRouter history={history}>
<Switch>
<Route path="/login" component={Login} />
<Route path="/" component={Main} />
</Switch>
</ConnectedRouter>
);
};
export default App;
Компонент Main
Main является компонентом контейнера без состояния, который будет перенаправлять на Login, если нет токена.
// Main.js
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { Route, Redirect } from 'react-router-dom';
const Main = ({ token }) => {
if (!token) {
return <Redirect to="/login" />;
}
return <div>Вы вошли в систему.</div>;
};
const mapStateToProps = (state) => ({
token: state.auth.token
});
export default connect(mapStateToProps)(Main);
Логин
Login - это компонент контейнера с сохранением состояния, который будет вызывать экшен AUTH_REQUEST.
// Login.js
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { authorize } from './reducer';
import { tokenSelector, errorSelector } from './selectors';
class Login extends PureComponent {
constructor(props) {
super(props);
this.state = { login: '', password: '' };
this.onChange = this.onChange.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
onChange(input, value) {
this.setState({ [input]: value });
}
onSubmit() {
const { login, password } = this.state;
this.props.dispatch(authorize(login, password));
}
render() {
const { error, token } = this.props;
if (token) {
return <Redirect to="/" />;
}
return (
<div>
<input
type='text'
placeholder='login'
value={this.state.login}
onChange={this.onChange.bind(this, 'login')}
/>
<input
type='password'
placeholder='password'
value={this.state.password}
onChange={this.onChange.bind(this, 'password')}
/>
<button onClick={this.onSubmit}>Submit</button>
</div>
);
}
}
const mapStateToProps = (state) => ({
token: state.auth.token,
error: state.auth.error
});
export default connect(mapStateToProps)(Login);
Вот и все, видите, как просто!