Redux saga how to stop action from further propagating - reactjs

I'm new to Redux Saga. I want to stop an action from further propagating. I am implementing row-level auto-saving mechanism. I use saga to detect row switch action, and then submit row changes and insert current row change action. codes like this:
// action-types.js
export const
SWITCH_ROW='SW_ROW',
CHANGE_CUR_ROW='CHG_CUR_ROW';
// actions.js
import {SWITCH_ROW,CHANGE_CUR_ROW} from './action-types'
export const switchRow=(oldRow,newRow)=>({type:SWITCH_ROW,oldRow,newRow})
export const changeRow=(row)=>({type:CHANGE_CUR_ROW,row})
// component.js
class MyComponent extends Component{
switchRow=(row)=>{
var oldRow=this.props.curRow;
this.props.dispatc(switchRow(oldRow,row));
}
render(){
...
{/* click on row */}
<div onClick={()=>this.switchRow(row)}>...</div>
...
}
}
// sagas.js
import {SWITCH_ROW} from './action-types'
import {changeRow} from './actions'
function* switchRow({oldRow,newRow}){
// Here I want to stop propagating SWITCH_ROW action further
// because this action is only designed to give saga a intervention
// point but not to be handled in reducer. I want a statement like
// below:
// stopPropogate();
if(oldRow && oldRow.modified===true){
yield call(svc.submit, oldRow);
}
yield put(changeRow(newRow))
}
export default function*(){
yield takeEvery(SWITCH_ROW,switchRow)
}
I know I can just ignore the SWITCH_ROW action in reducer. But, I think it's better if there is as least round trip as possible in program. Any suggests?

After more reading about Redux middle-ware, I think it's better to use a middle-ware to approach this goal.
At first, I renamed all type names of special actions for saga making them all starting with SAGA_. And then I use a Redux middle-ware to identify them and swallow them, and then those special actions can't reach reducer any more. Here is the codes:
// glutton.js
const glutton = () => next => action => {
if (!action.type.startsWith('SAGA_')) return next(action);
}
// store.js
...
const store = createStore(rootReducer, applyMiddleware(sagaMiddleWare, glutton, logger));

Lets say you are in a file which is not redux component, i mean you don't have access to dispatch , so i think throwing an error would be nice, and here is the code to catch the exception anywhere:
export default function autoRestart(generator) {
return function* autoRestarting(...args) {
while (true) {
try {
yield call(generator, ...args);
} catch (e) {
console.log(e);
yield put({ type: ReduxStates.Error });
}
}
}
};
all your sagas, need to implement this base function.

Related

How to get the new state with Redux-Saga?

I'm trying to get the new state to which the getCart() generator function returns me in reducer, but the state is coming "late".
The state I need comes only after the second click of the button.
NOTE: The error on the console I am forcing is an action.
import { call, put, select, all, takeLatest } from 'redux-saga/effects';
import { TYPES } from './reducer';
import { getCart, getCartSuccess, getCartFail } from './actions';
import API from 'services/JsonServerAPI';
export function* getCartList() {
try {
const response = yield call(API.get, '/2cart');
yield put(getCartSuccess(response.data));
} catch (error) {
yield put(
getCartFail(error.response ? error.response.statusText : error.message)
);
}
}
export function* addToCart({ id }) {
yield put(getCart());
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
// prettier-ignore
export default all([
takeLatest(TYPES.GET, getCartList),
takeLatest(TYPES.ADD, addToCart)
]);
Since getCartList performs async actions you will need some way to wait for those to complete in the addToCart before logging.
One option is to call the getCartList directly from the addToCart saga without dispatching a redux action - this may not be preferable if you have other middleware that relies on TYPES.GET being dispatched.
export function* addToCart({ id }) {
// call the `getCartList` saga directly and wait for it to finish before continuing
yield call(getCartList);
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
The other option is take on the list of actions that will be dispatched once the getCartList saga completes:
export function* addToCart({ id }) {
yield put(getCart());
// wait until one of the success or failure action is dispatched, sub with the proper types
yield take([TYPES.GET_SUCCESS, TYPES.GET_FAILURE]);
yield select(({ CartReducer }) => {
console.log(CartReducer);
});
console.log(id);
}
This has some potential tradeoffs as well - you will need to make sure the action list in take stays up to date with all possible ending types that getCartList can put and you need to make sure you keep using takeLatest (vs say takeEvery) to trigger addToCart so you don't end up with multiple concurrent sagas that could fulfill the take clause.

How to warn react component when redux saga put a success action?

I am using the redux action pattern (REQUEST, SUCCESS, FAILURE) along with redux saga. I made a watcher and worker saga just like that:
import axios from 'axios';
import { put, call, takeEvery } from 'redux-saga/effects';
import * as actionTypes from 'constants/actionTypes';
import * as actions from 'actions/candidates';
const { REQUEST } = actionTypes;
// each entity defines 3 creators { request, success, failure }
const { fetchCandidatesActionCreators, addCandidateActionCreators } = actions;
const getList = () => axios.get('/api/v1/candidate/');
// Watcher saga that spawns new tasks
function* watchRequestCandidates() {
yield takeEvery(actionTypes.CANDIDATES[REQUEST], fetchCandidatesAsync);
}
// Worker saga that performs the task
function* fetchCandidatesAsync() {
try {
const { data } = yield call(getList);
yield put(fetchCandidatesActionCreators.success(data.data));
} catch (error) {
yield put(fetchCandidatesActionCreators.failure(error));
}
}
const postCandidate = params => axios.post('/api/v1/candidate/', params).then(response => response.data).catch(error => { throw error.response || error.request || error; });
// Watcher saga that spawns new tasks
function* watchAddCandidate() {
yield takeEvery(actionTypes.ADD_CANDIDATE[REQUEST], AddCandidateAsync);
}
// Worker saga that performs the task
function* AddCandidateAsync({ payload }) {
try {
const result = yield call(postCandidate, payload);
yield put(addCandidateActionCreators.success(result.data));
} catch (error) {
yield put(addCandidateActionCreators.failure(error));
}
}
export default {
watchRequestCandidates,
fetchCandidatesAsync,
watchAddCandidate,
AddCandidateAsync,
};
My reducer has two flags: isLoading and success. Both flags change based on the request, success and failure actions.
The problem is that I want my component to render different things when the success action is put on the redux state. I want to warn the component every time a _success action happens!
The flags that I have work well on the first time, but then I want them to reset when the component mounts or a user clicks a button because my component is a form, and I want the user to post many forms to the server.
What is the best practice for that?
The only thing I could think of was to create a _RESET action that would be called when the user clicks the button to fill up other form and when the component mounts, but I don't know if this is a good practice.
You need to assign a higher order component, also called a Container, that connects the store with your component. When usgin a selector, your component will automatically update if that part of the state changes and passes that part of the state as a prop to your component. (as defined in dspatchstateToProps)
Down below i have a Exmaple component that select status from the redux state, and passes it as prop for Exmaple.
in example i can render different div elements with text based on the status shown in my store.
Good luck!
import { connect } from 'react-redux'
const ExampleComponent = ({ status }) => {
return (
<div>
{status === 'SUCCESS' ? (<div>yaay</div>) : (<div>oh no...</div>)}
</div>
)
}
const mapStateToProps = state => {
return {
status: state.status
}
}
const mapDispatchToProps = dispatch => {
return {}
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ExampleComponent)

React Redux Thunk trigger multiple actions on one call

I have an action which in turn must affect many other areas of my app state. In this case, when the user selects a website from a dropdown list, it must update many other components. I'm currently doing it like so:
setSelectedWebsite (websiteId) {
// The core reason for this component
this.props.setSelectedWebsite(websiteId);
// Fetch all associated accounts
this.props.fetchAllAccounts(websiteId)
// Several other "side effect" calls here...
}
In this interest of making one component serve one purpose, this feels like a bad practice.
What is the best practice for triggering multiple actions in one call from a component?
You could use redux-thunk.
Your component's method:
setSelectedWebsite(websiteId){
this.props.handleSetSelectedWebsite(websiteId) // this is a thunk
}
Your Redux file with action creators / thunks:
// this function is a thunk
export const handleSetSelectedWebsite = (websiteId) => (dispatch) => {
dispatch(setSelectedWebsite(websiteId))
dispatch(fetchAllAccounts(websiteId))
}
// these function are action creators (or could be other thunks if you style them the same way as the thunk above)
const setSelectedWebsite = (websiteId) => {
// your code
}
const fetchAllAccounts = (websiteId) => {
// your code
}
For handling complex side effects in a redux application, I would recommend looking at using Redux Sagas. I have seen its usage grow in popularity on projects large and small, and for good reason.
With sagas, in the example you have provided, you can emit a single action from a function provided through mapDispatchToProps and let a saga take care of the rest. For example: (following example assumes flux standard actions)
//import redux connect, react, etc
class SiteSelector extends React.Component {
render() {
const id = this.props.id;
return (
<button onClick={ () => this.props.action(id)>Click Me</button>
)
}
}
const mapStateToProps = (state) => ({
id: state.websiteId
})
const mapDispatchToProps = dispatch => ({
action: (id) => dispatch(setSelectedWebsite(id))
})
export connect(mapStateToProps, mapDispatchToProps)(SiteSelector)
Now you can handle the action emitted from setSelectedWebsite in a saga like so:
//import all saga dependencies, etc
export function* selectSite(action) {
const id = action.payload.id
yield put(Actions.selectWebsite(id))
const results = yield call(Api.fetchAllAccounts, id)
yield //do something complex with results
yield //do something else...
yield //and keep going...
}
// Our watcher Saga: spawn a new selectSite task every time the action from setSelectedWebsite is dispatched
export function* watchForSiteSelection() {
yield takeEvery('SITE_SELECTED_ACTION', selectSite)
}
For reference checkout the docs: Redux Sagas

Redux-saga calls action after cancel

I'm trying to implement React-boilerplate with redux-saga inside. So i'm trying to fetch some data from the server and then make a redirect to another page. The problem is that before redirecting saga makes second request to the server. I guess there is something wrong with cancelling it. Here is a part of my code:
export function* fetchData() {
...
console.log('fetched');
yield browserHistory.push('/another-page');
}
export function* fetchDataWatcher() {
while (yield take('FETCH_DATA')) {
yield call(fetchData);
}
}
export function* fetchDataRootSaga() {
const fetchWatcher = yield fork(fetchDataWatcher);
yield take(LOCATION_CHANGE);
yield cancel(fetchWatcher);
}
export default [
fetchDataRootSaga
]
So in this example i have two console logs, the second one appears before redirecting. How can i fix it?
And another question. Actually, i have more functions in this file. Should i create "rootSaga" for each of them or i can cancel them all in that fetchDataRootSaga()? I mean is it normal if i cancel sagas this way:
export function* fetchDataRootSaga() {
const watcherOne = yield fork(fetchDataOne);
const watcherTwo = yield fork(fetchDataTwo);
...
yield take(LOCATION_CHANGE);
yield cancel(watcherOne);
yield cancel(watcherTwo);
...
}
Thanks in advance!
P.S. I'm not sure if this code is best practices. It is inspired by this repository
Maybe start by adjusting your loop inside fetchDataWatcher to look a little more like this
export function* fetchDataWatcher() {
while (true) {
yield take('FETCH_DATA');
yield call(fetchData);
}
}
Also you can route better by doing something like this perhaps
import { push } from 'react-router-redux';
import { put } from 'redux-saga/effects';
export function* fetchData() {
...
console.log('fetched');
yield put(push('/another-page'));
}
Overall I would hesitate to put a route change and then altogether separately do a take on it, only if you wish to cancel on all location changes (but I assume that's what you're after :) )
This defeats the purpose of saga, which is to handle potentially long running async requests and returns. You could instead set a state in your redux store like so
export function* fetchData() {
...
console.log('fetched');
yield put(setRedirectState('/another-page'));
}
Then see if the redirect state is set in your container in ComponentWillUpdate and redirect accordingly to something like this
import { push } from 'react-router-redux';
dispatch(push(state.redirecturl))
I haven't tried this, but the experience I have with React-boilerplate, this is what I would try first.

Correct way to throttle HTTP calls based on state in redux and react

What I need:
A controlled component that is an input[type="search"]. After say, 1 second of no changes to that component I want to send out an HTTP call to execute that search and have the results shown in another component.
How I've done it:
Whenever I call dispatch(setSearch(str)); I make a call to dispatch(displaySearch(false)); and then _.throttle a call to dispatch(displaySearch(true));
It feels to me that doing this sort of work in the component is incorrect, but I can't think of a way to do this in a reducer in redux.
You have different options to solve this.
1. Debounce your action at a component level
This is the simplest approach. When the input triggers a change, it calls
a debounced version of setSearch delaying the server call.
import * as React from "react"
import {connect} from "react-redux"
import {setSearch} from "./actions"
export default connect(
null,
function mapDispatchToProps(dispatch) {
const setSearch_ = _.debounce(q => dispatch(setSearch(q)), 1000)
return () => ({setSearch: setSearch_})
}
)(
function SearchForm(props) {
const {setSearch} = props
return (
<input type="search" onChange={setSearch} />
)
}
)
2. Debounce using redux-saga
This approach requires more boilerplate but gives you a lot more control over
the workflow. Using a saga we intercept the SET_SEARCH action, debounce it,
call the API then dispatch a new action containing the results.
import {call, cancel, fork, put, take} from "redux-saga/effects"
import {setSearchResults} from "./actions"
import {api} from "./services"
import {delay} from "./utils"
export default function* searchSaga() {
yield [
// Start a watcher to handle search workflow
fork(watchSearch)
]
}
function* watchSearch() {
let task
// Start a worker listening for `SET_SEARCH` actions.
while (true) {
// Read the query from the action
const {q} = yield take("SET_SEARCH")
// If there is any pending search task then cancel it
if (task) {
yield cancel(task)
}
// Create a worker to proceed search
task = yield fork(handleSearch, q)
}
}
function* handleSearch(q) {
// Debounce by 1s. This will lock the process for one second before
// performing its logic. Since the process is blocked, it can be cancelled
// by `watchSearch` if there are any other actions.
yield call(delay, 1000)
// This is basically `api.doSearch(q)`. The call should return a `Promise`
// that will resolve the server response.
const results = yield call(api.doSearch, q)
// Dispatch an action to notify the UI
yield put(setSearchResults(results))
}
Associate delay and takeLatest:
import { all, takeLatest, call } from 'redux-saga/effects';
import { delay } from 'redux-saga';
function* onSearch(action) {
yield call(delay, 1000); // blocks until a new action is
// triggered (takeLatest) or until
// the delay of 1s is over
const results = yield call(myFunction);
}
function* searchSagas() {
yield all([
// ...
takeLatest(ON_SEARCH, onSearch),
]);
}
export default [searchSagas];

Resources