React and Redux Sagas Authentication App Tutorial 1
React and Redux Sagas Authentication App Tutorial Part 2
React and Redux Sagas Authentication App Tutorial Part 3
28) Redux Saga implementation – Learn React js in Tamil (Time line : 13:00)
Define mapStateToProps: This function maps the Redux store state to props that your component can access. It takes the Redux state as an argument and returns an object with props.
const mapStateToProps = (state) => {
return {
counter: state.counter, // Assuming you have a "counter" state in your store
// Other props you want to map
};
};
Define mapDispatchToProps: This function maps action dispatchers to props. It takes the dispatch function as an argument and returns an object with props.
const mapDispatchToProps = (dispatch) => {
return {
increment: () => dispatch({ type: 'INCREMENT' }),
decrement: () => dispatch({ type: 'DECREMENT' }),
// Other action dispatchers you want to map
};
};
Connect your component: Use the connect function to connect your component to the Redux store, passing in mapStateToProps and mapDispatchToProps.
class CounterComponent extends React.Component {
// Your component code here
}
// Connect the component using the connect HOC
export default connect(mapStateToProps, mapDispatchToProps)(CounterComponent);
Now, your CounterComponent will have access to the Redux store state and the action dispatchers you defined as props. You can access the state using this.props.counter and call the action dispatchers like this.props.increment() and this.props.decrement().
Remember that with the introduction of React Hooks, you can also use the useSelector and useDispatch hooks from the react-redux library as an alternative to mapStateToProps and mapDispatchToProps.
import { useSelector, useDispatch } from 'react-redux';
function CounterComponent() {
const counter = useSelector(state => state.counter);
const dispatch = useDispatch();
const increment = () => {
dispatch({ type: 'INCREMENT' });
};
const decrement = () => {
dispatch({ type: 'DECREMENT' });
};
// Your component code here
}
2. connected-react-router :
‘connected-react-router’ is a library that helps you integrate React Router with Redux in your React applications.
By using ‘connected-react-router’, you can keep your routing state in sync with your Redux store, making it easier to manage and control navigation-related actions and state changes.
In this example, the ‘connectRouter’ function from ‘connected-react-router’ is used to integrate the router state into the Redux store. The ‘routerMiddleware’ is used to capture route changes and dispatch appropriate actions to keep the router state in sync.
all, takeEvery, put in redux-saga
–‘all’ is used to run multiple sagas concurrently.
–‘takeEvery’ is used to listen for specific actions and execute a saga every time that action is dispatched.
–‘put’ is used to dispatch actions from within your saga to communicate with the Redux store.
–‘call’ providing a way to invoke asynchronous and side-effectful functions
–‘withRouter’ allows you to access the routing props in components, without the need to explicitly pass them down through component nesting.
export default withRouter(MyComponent);
const { match, location, history } = this.props;
Now, MyComponent has access to the routing props (match, location, and history) even though it’s not directly rendered within a Route component.
Remember that with the introduction of React Hooks, you can often achieve similar functionality using the useParams, useLocation, and useHistory hooks provided by react-router-dom without the need for withRouter.
In react-router-dom, ‘withRouter’ is a higher-order component (HOC) that is used to wrap a component and provide it with access to the routing props (such as match, location, and history) without the need to explicitly pass them down through component nesting.
— The ‘location’ object contains information about the current URL location, including pathname, search, hash, and state. This allows you to access and manipulate the current URL and associated data.
— Remember that the location prop is only available when you use the Route component’s render prop. If you’re using the component prop to render a component, the location prop will not be automatically passed. However, you can use the withRouter higher-order component to inject the location prop into the component’s props even when using the component prop.
—–
Layouts :
we have two options for using layouts:
1. Making every component wrapped by layout separately
2. Wrapping all routes that you want to use layout with layout component
(I prefer the first way, I’m not so comfortable with the second one)
What is Redux Persist?
Redux Persist is a library that allows saving a Redux store in the local storage of an application.
For persistReducer is wrap your app’s root reducers and pass it to the persistStore function it ensures your redux state is stored to persisted storage whenever it changes
store.js
PersistGate delays the rendering of your app’s UI until your persisted state has been retrieved and saved to redux.
Redux Saga Example
src\index.js
src\App.js
src\index-reducer.jss
src\index-sagas.js
src\login\constants.js
src\login\actions.js
src\login\reducer.js
src\login\sagas.js
src\login\index.js
——–
1- src\index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { applyMiddleware, createStore, compose } from 'redux'
import { Provider } from 'react-redux'
import createSagaMiddleware from 'redux-saga'
import { Router, Route, browserHistory, IndexRoute } from 'react-router'
// add IndexRoute above and the helpers below
import {
checkIndexAuthorization,
checkWidgetAuthorization,
} from './lib/check-auth'
// Import all of our components
import App from './App'
import Login from './login'
import Signup from './signup'
import Widgets from './widgets'
import './index.css'
// Import the index reducer and sagas
import IndexReducer from './index-reducer'
import IndexSagas from './index-sagas'
// Setup the middleware to watch between the Reducers and the Actions
const sagaMiddleware = createSagaMiddleware()
// Redux DevTools - completely optional, but this is necessary for it to
// work properly with redux saga. Otherwise you'd just do:
//
// const store = createStore(
// IndexReducer,
// applyMiddleware(sagaMiddleware)
// )
/*eslint-disable */
const composeSetup = process.env.NODE_ENV !== 'production' && typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose
/*eslint-enable */
const store = createStore(
IndexReducer,
composeSetup(applyMiddleware(sagaMiddleware)), // allows redux devtools to watch sagas
)
// Begin our Index Saga
sagaMiddleware.run(IndexSagas)
ReactDOM.render(
<Provider store={store}>
<Router history={browserHistory}>
<Route path="/" component={App} >
<IndexRoute onEnter={checkIndexAuthorization(store)} />
<Route path="/login" component={Login} />
<Route path="/signup" component={Signup} />
<Route onEnter={checkWidgetAuthorization(store)} path="/widgets" component={Widgets} />
</Route>
</Router>
</Provider>,
document.getElementById('root'),
)
2- src\App.js
import React, { PropTypes } from 'react'
import logo from './logo.svg'
import './App.css'
const App = props => (
<div className="App">
<div className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h2>Welcome to Widget Reactory</h2>
</div>
<section className="App-body">
{props.children}
</section>
</div>
)
App.propTypes = {
children: PropTypes.node,
}
export default App
3- src\index-reducer.js
import { combineReducers } from 'redux'
import { reducer as form } from 'redux-form'
import client from './client/reducer'
import signup from './signup/reducer'
import login from './login/reducer'
import widgets from './widgets/reducer'
const IndexReducer = combineReducers({
signup,
client,
login,
form,
widgets,
})
export default IndexReducer
4- src\index-sagas.js
import SignupSaga from './signup/sagas'
import LoginSaga from './login/sagas'
import WidgetSaga from './widgets/sagas'
export default function* IndexSaga () {
yield [
SignupSaga(),
LoginSaga(),
WidgetSaga(),
]
}
5- src\login\constants.js
export const LOGIN_REQUESTING = 'LOGIN_REQUESTING'
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS'
export const LOGIN_ERROR = 'LOGIN_ERROR'
export const LOGIN_EXISTING = 'LOGIN_EXISTING'
export const LOGOUT = 'LOGOUT'
6- src\login\actions.js
import {
LOGIN_REQUESTING,
} from './constants'
// In order to perform an action of type LOGIN_REQUESTING
// we need an email and password
const loginRequest = function loginRequest ({ email, password }) {
return {
type: LOGIN_REQUESTING,
email,
password,
}
}
// Since it's the only one here
export default loginRequest
7- src\login\reducer.js
import {
LOGIN_REQUESTING,
LOGIN_SUCCESS,
LOGIN_ERROR,
} from './constants'
const initialState = {
requesting: false,
successful: false,
messages: [],
errors: [],
}
const reducer = function loginReducer (state = initialState, action) {
switch (action.type) {
// Set the requesting flag and append a message to be shown
case LOGIN_REQUESTING:
return {
requesting: true,
successful: false,
messages: [{ body: 'Logging in...', time: new Date() }],
errors: [],
}
// Successful? Reset the login state.
case LOGIN_SUCCESS:
return {
errors: [],
messages: [],
requesting: false,
successful: true,
}
// Append the error returned from our api
// set the success and requesting flags to false
case LOGIN_ERROR:
return {
errors: state.errors.concat([{
body: action.error.toString(),
time: new Date(),
}]),
messages: [],
requesting: false,
successful: false,
}
default:
return state
}
}
export default reducer
8- src\login\sagas.js
import { take, fork, cancel, call, put, cancelled } from 'redux-saga/effects'
// We'll use this function to redirect to different routes based on cases
import { browserHistory } from 'react-router'
// Helper for api errors
import { handleApiErrors } from '../lib/api-errors'
// Our login constants
import {
LOGIN_REQUESTING,
LOGIN_SUCCESS,
LOGIN_ERROR,
} from './constants'
// So that we can modify our Client piece of state
import {
setClient,
unsetClient,
} from '../client/actions'
import {
CLIENT_UNSET,
} from '../client/constants'
const loginUrl = `${process.env.REACT_APP_API_URL}/api/Clients/login`
function loginApi (email, password) {
return fetch(loginUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ email, password }),
})
.then(handleApiErrors)
.then(response => response.json())
.then(json => json)
.catch((error) => { throw error })
}
function* logout () {
// dispatches the CLIENT_UNSET action
yield put(unsetClient())
// remove our token
localStorage.removeItem('token')
// redirect to the /login screen
browserHistory.push('/login')
}
function* loginFlow (email, password) {
let token
try {
// try to call to our loginApi() function. Redux Saga
// will pause here until we either are successful or
// receive an error
token = yield call(loginApi, email, password)
// inform Redux to set our client token, this is non blocking so...
yield put(setClient(token))
// .. also inform redux that our login was successful
yield put({ type: LOGIN_SUCCESS })
// set a stringified version of our token to localstorage on our domain
localStorage.setItem('token', JSON.stringify(token))
// redirect them to WIDGETS!
browserHistory.push('/widgets')
} catch (error) {
// error? send it to redux
yield put({ type: LOGIN_ERROR, error })
} finally {
// No matter what, if our `forked` `task` was cancelled
// we will then just redirect them to login
if (yield cancelled()) {
browserHistory.push('/login')
}
}
// return the token for health and wealth
return token
}
// Our watcher (saga). It will watch for many things.
function* loginWatcher () {
// Generators halt execution until their next step is ready/occurring
// So it's not like this loop is firing in the background 1000/sec
// Instead, it says, "okay, true === true", and hits the first step...
while (true) {
//
// ... and in this first it sees a yield statement with `take` which
// pauses the loop. It will sit here and WAIT for this action.
//
// yield take(ACTION) just says, when our generator sees the ACTION
// it will pull from that ACTION's payload that we send up, its
// email and password. ONLY when this happens will the loop move
// forward...
const { email, password } = yield take(LOGIN_REQUESTING)
// ... and pass the email and password to our loginFlow() function.
// The fork() method spins up another "process" that will deal with
// handling the loginFlow's execution in the background!
// Think, "fork another process".
//
// It also passes back to us, a reference to this forked task
// which is stored in our const task here. We can use this to manage
// the task.
//
// However, fork() does not block our loop. It's in the background
// therefore as soon as our loop executes this it mores forward...
const task = yield fork(loginFlow, email, password)
// ... and begins looking for either CLIENT_UNSET or LOGIN_ERROR!
// That's right, it gets to here and stops and begins watching
// for these tasks only. Why would it watch for login any more?
// During the life cycle of this generator, the user will login once
// and all we need to watch for is either logging out, or a login
// error. The moment it does grab either of these though it will
// once again move forward...
const action = yield take([CLIENT_UNSET, LOGIN_ERROR])
// ... if, for whatever reason, we decide to logout during this
// cancel the current action. i.e. the user is being logged
// in, they get impatient and start hammering the logout button.
// this would result in the above statement seeing the CLIENT_UNSET
// action, and down here, knowing that we should cancel the
// forked `task` that was trying to log them in. It will do so
// and move forward...
if (action.type === CLIENT_UNSET) yield cancel(task)
// ... finally we'll just log them out. This will unset the client
// access token ... -> follow this back up to the top of the while loop
yield call(logout)
}
}
export default loginWatcher
9- src\login\index.js
import React, { Component, PropTypes } from 'react'
import { reduxForm, Field } from 'redux-form'
import { connect } from 'react-redux'
import { Link } from 'react-router'
import Messages from '../notifications/Messages'
import Errors from '../notifications/Errors'
import loginRequest from './actions'
// If you were testing, you'd want to export this component
// so that you can test your custom made component and not
// test whether or not Redux and Redux Form are doing their jobs
class Login extends Component {
// Pass the correct proptypes in for validation
static propTypes = {
handleSubmit: PropTypes.func,
loginRequest: PropTypes.func,
login: PropTypes.shape({
requesting: PropTypes.bool,
successful: PropTypes.bool,
messages: PropTypes.array,
errors: PropTypes.array,
}),
}
// Remember, Redux Form passes the form values to our handler
// In this case it will be an object with `email` and `password`
submit = (values) => {
this.props.loginRequest(values)
}
render () {
const {
handleSubmit, // remember, Redux Form injects this into our props
login: {
requesting,
successful,
messages,
errors,
},
} = this.props
return (
<div className="login">
<form className="widget-form" onSubmit={handleSubmit(this.submit)}>
<h1>LOGIN</h1>
<label htmlFor="email">Email</label>
{/*
Our Redux Form Field components that bind email and password
to our Redux state's form -> login piece of state.
*/}
<Field
name="email"
type="text"
id="email"
className="email"
component="input"
/>
<label htmlFor="password">Password</label>
<Field
name="password"
type="password"
id="password"
className="password"
component="input"
/>
<button action="submit">LOGIN</button>
</form>
<div className="auth-messages">
{/* As in the signup, we're just using the message and error helpers */}
{!requesting && !!errors.length && (
<Errors message="Failure to login due to:" errors={errors} />
)}
{!requesting && !!messages.length && (
<Messages messages={messages} />
)}
{requesting && <div>Logging in...</div>}
{!requesting && !successful && (
<Link to="/signup">Need to Signup? Click Here »</Link>
)}
</div>
</div>
)
}
}
// Grab only the piece of state we need
const mapStateToProps = state => ({
login: state.login,
})
// make Redux state piece of `login` and our action `loginRequest`
// available in this.props within our component
const connected = connect(mapStateToProps, { loginRequest })(Login)
// in our Redux's state, this form will be available in 'form.login'
const formed = reduxForm({
form: 'login',
})(connected)
// Export our well formed login component
export default formed