diff --git a/README.md b/README.md index c358b49..9d220b4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # React Redux Login Flow A simple application to demonstrate a login flow with React & Redux. -Read full tutorial at here: ![A simple login flow with React and Redux](http://jslancer.com/?p=1213&preview=true) +Read full tutorial at here: [A simple login flow with React and Redux](http://jslancer.com/2017/04/27/a-simple-login-flow-with-react-and-redux) ![ezgif com-video-to-gif](https://cloud.githubusercontent.com/assets/1154740/25493010/fed3b2cc-2b9e-11e7-8736-9250549128ec.gif) @@ -12,3 +12,6 @@ npm intall npm start ``` +# Example +Login: admin@example.com +Password: admin diff --git a/package.json b/package.json index 78afb16..af177a1 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "react-redux-login-flow", - "version": "0.1.0", + "version": "0.2.0", "private": true, "dependencies": { + "fetch-middleware": "^1.0.4", "react": "^15.5.4", "react-dom": "^15.5.4", "react-redux": "^5.0.4", "redux": "^3.6.0", - "redux-logger": "^3.0.1", - "redux-thunk": "^2.2.0" + "redux-actions": "^2.0.2", + "redux-logger": "^3.0.1" }, "devDependencies": { "react-scripts": "0.9.5" diff --git a/src/components/LoginForm/LoginForm.css b/src/assets/LoginForm.css similarity index 100% rename from src/components/LoginForm/LoginForm.css rename to src/assets/LoginForm.css diff --git a/src/components/LoginForm.js b/src/components/LoginForm.js new file mode 100644 index 0000000..0a77795 --- /dev/null +++ b/src/components/LoginForm.js @@ -0,0 +1,40 @@ +import React from 'react' +import '../assets/LoginForm.css' + +const LoginForm = props => { + const {email, password, text, error, handleSubmit, handleFieldChange} = props + return ( +
+
+
+ + +
+ +
+ + +
+
+ + + +
+ {text} + {error && error.message} +
+
+ ) +} + +export default LoginForm diff --git a/src/components/LoginForm/LoginForm.js b/src/components/LoginForm/LoginForm.js deleted file mode 100644 index 915333b..0000000 --- a/src/components/LoginForm/LoginForm.js +++ /dev/null @@ -1,67 +0,0 @@ -import React, { Component } from 'react'; -import { connect } from 'react-redux'; -import { login } from '../../redux/reducer'; -import './LoginForm.css'; - -class LoginForm extends Component { - - constructor(props) { - super(props); - this.state = {}; - this.onSubmit = this.onSubmit.bind(this); - } - - render() { - let {email, password} = this.state; - let {isLoginPending, isLoginSuccess, loginError} = this.props; - return ( -
-
-
- - this.setState({email: e.target.value})} value={email}/> -
- -
- - this.setState({password: e.target.value})} value={password}/> -
-
- - - -
- { isLoginPending &&
Please wait...
} - { isLoginSuccess &&
Success.
} - { loginError &&
{loginError.message}
} -
-
- ) - } - - onSubmit(e) { - e.preventDefault(); - let { email, password } = this.state; - this.props.login(email, password); - this.setState({ - email: '', - password: '' - }); - } -} - -const mapStateToProps = (state) => { - return { - isLoginPending: state.isLoginPending, - isLoginSuccess: state.isLoginSuccess, - loginError: state.loginError - }; -} - -const mapDispatchToProps = (dispatch) => { - return { - login: (email, password) => dispatch(login(email, password)) - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(LoginForm); \ No newline at end of file diff --git a/src/containers/Login.js b/src/containers/Login.js new file mode 100644 index 0000000..265d269 --- /dev/null +++ b/src/containers/Login.js @@ -0,0 +1,56 @@ +import React, {Component} from 'react' +import {connect} from 'react-redux' +import {bindActionCreators} from 'redux' + +import {login} from '../redux/actions' +import LoginForm from '../components/LoginForm' + +const emptyForm = () => ({ + email: '', + password: '' +}) + +class Login extends Component { + constructor(props) { + super(props) + this.state = emptyForm() + } + + handleSubmit = e => { + e.preventDefault() + const {email, password} = this.state + this.props.login({email, password}) + this.setState(emptyForm) + } + + handleFieldChange = e => { + this.setState({ + [e.target.name]: e.target.value + }) + } + + render() { + const {email, password} = this.state + const {text, error} = this.props.Login + return ( +
+ +
+ ) + } +} + +const mapStateToProps = state => ({ + Login: state +}) + +const mapDispatchToProps = dispatch => bindActionCreators({login}, dispatch) + +export default connect(mapStateToProps, mapDispatchToProps)(Login) diff --git a/src/index.js b/src/index.js index 813f834..8adea6e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,12 +1,13 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import LoginForm from './components/LoginForm/LoginForm'; -import { Provider } from 'react-redux'; -import store from './redux/store'; +import React from 'react' +import {render} from 'react-dom' +import {Provider} from 'react-redux' -ReactDOM.render( +import LoginForm from './containers/Login' +import {store} from './redux/store' + +render( , document.getElementById('root') -); +) diff --git a/src/redux/actions.js b/src/redux/actions.js new file mode 100644 index 0000000..dfce2b4 --- /dev/null +++ b/src/redux/actions.js @@ -0,0 +1,27 @@ +import {createAction} from 'redux-actions' + +export const SET_LOGIN_PENDING = 'modules/Login/PENDING' +export const SET_LOGIN_SUCCESS = 'modules/Login/SUCCESS' +export const SET_LOGIN_ERROR = 'modules/Login/ERROR' + +const loginPending = createAction(SET_LOGIN_PENDING) +const loginSuccess = createAction(SET_LOGIN_SUCCESS) +const loginError = createAction(SET_LOGIN_ERROR) + +const fakeLoginApi = values => { + return new Promise((resolve, reject) => { + setTimeout(() => { + if (values.email !== 'admin@example.com' || values.password !== 'admin') { + return reject(new Error('Invalid email and password')) + } + return resolve(true) + }, 1000) + }) +} + +export const login = values => ({ + type: [loginPending, loginSuccess, loginError], + payload: { + data: () => fakeLoginApi(values) + } +}) diff --git a/src/redux/reducer.js b/src/redux/reducer.js index d958b37..6ab3588 100644 --- a/src/redux/reducer.js +++ b/src/redux/reducer.js @@ -1,77 +1,35 @@ -const SET_LOGIN_PENDING = 'SET_LOGIN_PENDING'; -const SET_LOGIN_SUCCESS = 'SET_LOGIN_SUCCESS'; -const SET_LOGIN_ERROR = 'SET_LOGIN_ERROR'; +import {handleActions} from 'redux-actions' -export function login(email, password) { - return dispatch => { - dispatch(setLoginPending(true)); - dispatch(setLoginSuccess(false)); - dispatch(setLoginError(null)); +import {SET_LOGIN_PENDING, SET_LOGIN_SUCCESS, SET_LOGIN_ERROR} from './actions' - callLoginApi(email, password, error => { - dispatch(setLoginPending(false)); - if (!error) { - dispatch(setLoginSuccess(true)); - } else { - dispatch(setLoginError(error)); - } - }); - } +const initalState = { + requestPending: false, + text: null, + error: false } -function setLoginPending(isLoginPending) { - return { - type: SET_LOGIN_PENDING, - isLoginPending - }; -} - -function setLoginSuccess(isLoginSuccess) { - return { - type: SET_LOGIN_SUCCESS, - isLoginSuccess - }; -} - -function setLoginError(loginError) { - return { - type: SET_LOGIN_ERROR, - loginError - } -} - -function callLoginApi(email, password, callback) { - setTimeout(() => { - if (email === 'admin@example.com' && password === 'admin') { - return callback(null); - } else { - return callback(new Error('Invalid email and password')); - } - }, 1000); -} - -export default function reducer(state = { - isLoginSuccess: false, - isLoginPending: false, - loginError: null -}, action) { - switch (action.type) { - case SET_LOGIN_PENDING: - return Object.assign({}, state, { - isLoginPending: action.isLoginPending - }); - - case SET_LOGIN_SUCCESS: - return Object.assign({}, state, { - isLoginSuccess: action.isLoginSuccess - }); - - case SET_LOGIN_ERROR: - return Object.assign({}, state, { - loginError: action.loginError - }); - - default: - return state; - } -} \ No newline at end of file +const reducer = handleActions( + { + [SET_LOGIN_PENDING]: (state, action) => ({ + ...state, + requestPending: true, + text: 'Please wait...', + error: false + }), + [SET_LOGIN_SUCCESS]: (state, action) => ({ + ...state, + requestPending: false, + text: 'Success', + error: false + }), + [SET_LOGIN_ERROR]: (state, action) => ({ + ...state, + requestPending: false, + text: null, + error: action.payload + }) + }, + initalState +) + +export default reducer diff --git a/src/redux/store.js b/src/redux/store.js index b195a9a..a6e5258 100644 --- a/src/redux/store.js +++ b/src/redux/store.js @@ -1,7 +1,12 @@ -import { createStore, applyMiddleware } from 'redux'; -import thunk from 'redux-thunk'; -import logger from 'redux-logger'; -import reducer from './reducer'; +import {createStore, applyMiddleware, compose} from 'redux' +import fetchMiddleware from 'fetch-middleware' +import logger from 'redux-logger' +import Login from './reducer' -const store = createStore(reducer, {}, applyMiddleware(thunk, logger)); -export default store; \ No newline at end of file +const devTools = window.devToolsExtension ? window.devToolsExtension() : f => f + +export const store = createStore( + Login, + {}, + compose(applyMiddleware(fetchMiddleware, logger), devTools) +)