I'm using the react-boilerplate injectSaga/ejectSaga functionality (reference) and I'm running into an issue trying to cancel my saga.
tl;dr - When the component with the injected saga is unmounted, the saga cancellation occurs via task.cancel(), but this isn't triggering in the actual saga itself (i.e., yield cancelled() is not triggering)
I inject my saga into my container like so (using react-router v4 and redux):
const enhance = compose(
withRouter, // from react-router 4
injectSaga({ key: 'stuffDirectory', saga: stuffDirectorySaga }),
connect(mapStateToProps, mapDispatchToProps),
// other things that we're composing
);
The injectSaga function performs the following magic:
export default ({ key, saga, mode }) => WrappedComponent => {
class InjectSaga extends React.Component {
static WrappedComponent = WrappedComponent;
static contextTypes = {
store: PropTypes.object.isRequired,
};
static displayName = `withSaga(${WrappedComponent.displayName ||
WrappedComponent.name ||
'Component'})`;
componentWillMount() {
const { injectSaga } = this.injectors;
injectSaga(key, { saga, mode }, this.props);
}
componentWillUnmount() {
const { ejectSaga } = this.injectors;
ejectSaga(key);
}
injectors = getInjectors(this.context.store);
render() {
return <WrappedComponent {...this.props} />;
}
}
return hoistNonReactStatics(InjectSaga, WrappedComponent);
};
Here is my saga (pretty straightforward):
export function* watchSizesDraftsSaga() {
try {
let stuffWeLookingFor = yield select(selectStuff());
// waiting for stuff here
yield put( moveStuffOver(stuffWeLookingFor) );
// everything works fine here, page works as expected and stuff has been found
} finally {
if (yield cancelled()) {
// PROBLEM: when the saga is ejected via ejectSaga, this condition never happens
yield put( cleanStuffUp() );
}
}
}
The saga injection works correctly - every time the component mounts, I can clearly see the actions being called and caught in redux. In the specific sagaInjectors.js file from react-boilerplate, I've found the specific line that cancels injected sagas:
if (Reflect.has(store.injectedSagas, key)) {
const descriptor = store.injectedSagas[key];
if (descriptor.mode && descriptor.mode !== DAEMON) {
// the task is cancelled here
descriptor.task.cancel();
// we verify that the task is cancelled here - descriptor.task.isCancelled() returns true if we log it
// Clean up in production; in development we need `descriptor.saga` for hot reloading
if (process.env.NODE_ENV === 'production') {
// Need some value to be able to detect `ONCE_TILL_UNMOUNT` sagas in `injectSaga`
store.injectedSagas[key] = 'done'; // eslint-disable-line no-param-reassign
}
}
}
My yield cancelled() block is never running despite the task successfully being cancelled. Is there something I'm missing regarding how saga cancellation works? Is there an easy way for me to easily handle my saga clean up logic for whenever a saga is about to be cancelled?
I think you misunderstand how task cancellation works in redux-saga.
Your task corresponding to watchSizesDraftsSaga() completes on its own.
Then task.cancel() does nothing.
yield cancelled() block will only execute if you cancel a task when it is still running.
Related
I am new in react js. I have started doing a small product with react-redux. I am using saga middle-ware.
What i have done is as under.
This is the component
//all import work
import { activateAuthLayout, onLoad } from '../../../store/actions';
class EcommerceProductEdit extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
unselected_lists: [],
main_checked: false
}
//here I get the products props always null
console.log(this.props);
}
componentDidMount() {
this.props.activateAuthLayout();
//dispatching an action to fetch data from api, done in midddleware
if (this.props.user !== null && this.props.user.shop_id)
this.props.onLoad({
payload: this.props.user
});
}
render() {
//here I get the products props
console.log(this.props);
return (
//jsx work
);
}
}
const mapStatetoProps = state => {
const { user, is_logged_in } = state.Common;
const { products, is_loading } = state.Products;
return { user, is_logged_in, products, is_loading };
}
export default withRouter(connect(mapStatetoProps, { activateAuthLayout, onLoad })(EcommerceProductEdit));
Action is
import { FETCH_PRODUCT, FETCH_PRODUCT_SUCCESS } from './actionTypes';
export const onLoad = (action) => {
return {
type: FETCH_PRODUCT,
payload: action.payload
}
}
export const productFetched = (action) => {
return {
type: FETCH_PRODUCT_SUCCESS,
payload: action.payload
}
}
Reducer is
import { FETCH_PRODUCT_SUCCESS } from './actionTypes';
const initialState = {
products: null,
is_loading: true
}
export default (state = initialState, action) => {
switch (action.type) {
case FETCH_PRODUCT_SUCCESS:
state = {
...state,
products: action.payload,
is_loading: false
}
break;
default:
state = { ...state };
break;
}
return state;
}
And saga is
import { takeEvery, put, call } from 'redux-saga/effects';
import { FETCH_PRODUCT } from './actionTypes';
import { productFetched } from './actions';
import agent from '../../agent';
function* fetchProduct(action) {
try {
let response = yield call(agent.Products.get, action.payload);
yield put(productFetched({ payload: response }));
} catch (error) {
if (error.message) {
console.log(error);
} else if (error.response.text === 'Unauthorized') {
console.log(error)
}
}
}
function* productSaga() {
yield takeEvery(FETCH_PRODUCT, fetchProduct)
}
export default productSaga;
I am being able to get the products props only in render function. How would i be able to get it it in constructor ?
I would be really grateful if anyone explained me about react life cycle a little bit more.
Thanks.
updated
a constructor is called during object instantiation. According to the docs "The constructor for a React component is called before it is mounted". So if the props passed to the component are being changed after the component has been mounted you can use componentWillReceiveProps life cycle methods.
componentWillReceiveProps is deprecated so you can use componentDidUpdate instead. Example from the docs.
componentDidUpdate(prevProps) {
// Typical usage (don't forget to compare props):
if (this.props.userID !== prevProps.userID) {
// update your component state from here.
this.fetchData(this.props.userID);
}
}
MiddleWare: Middleware just comes in between the flow after the action has been dispatched and before it reaches the reducers, like in your case once you fire onLoad action and before it reaches the reducers, its caught in Saga middleware which executes it according to code written in it
Lifecycle in your case goes the following way:
In your compoenentDidMount method, you dispatch an action of onLoad. The action type in such a case becomes "FETCH_PRODUCT" and same action is now caught in Saga.
Since this is async call, the code in your component continues executing while the Saga perform its action in parallel. It calls API through this line of code: yield call(agent.Products.get, action.payload); . Once API call is completed, it dispatches an action 'productfetched' through this line of code yield put(productFetched({ payload: response }));.
Now this action reaches reducer and modify the state of "products". Since the product state in your redux is modified, your component EcommerceProductEdit re-renders and you get your product list in render method. The point to be noted is that the flow must have already finished executing inside componentDidMount method by this time, so no chance of having products their
Solution to your problem:
Once an action is dispatched and which has become async due to Saga, you won't be able to get value in constructor, if you use Saga. You can just directly call upon the API using axios/fetch library in componentDidMount and await their (Making it synchronous). Once you get response, you may proceed further
In case you have functional component, then you may use Effect hook and bind the dependency to products state. You can write your code in this block, what you want to be executed after API call is made and product list modifies.
React.useEffect(
() => {
// You code goes here
},
[products]
);
You just have to console props rather than doing this.props. You should not reference props with this inside the constructor.
Do this instead:
console.log(props)
Middleware is not related to react lifecycle at all, other than it updates and connected components "react" to props updating.
Check the constructor docs
https://reactjs.org/docs/react-component.html#constructor
Question: why are you trying to log props in the constructor anyway? If you want to know what the props are, use one of the lifecycle functions, componentDidMount/componentDidUpdate, don't use the render function to do side-effects like make asynchronous calls or console log.
componentDidMount() {
console.log(this.props);
}
If you must log props in the constructor though, access the props object that was passed as the component won't have a this.props populated yet.
constructor(props) {
super(props);
...
console.log(props);
}
I have a React JS app, which calls a Redux Saga in order to access a rest API resource. The idea is to call a resource with certain uuid and this call returns the next uuid for the next call. When the props change in React, the componentDidUpdate method launches the action that deploys the saga. It does fine for the first time, then the second call ignores the saga with no error and no warning. Here is what I have:
export class MyComp extends React.Component {
...
componentDidUpdate(prevProps, prevState) {
if (prevProps.uuid !== this.props.uuid) {
this.props.actions.fetchData(this.props.uuid);
}
}
...
}
These are the actions:
export function fetchData(uuid) {
return {
type: FETCH_DATA_REQUEST,
payload: { uuid }
};
}
export function dataFetched(uuid, data) {
return {
type: DATA_FETCHED,
payload: {uuid, data}
};
}
and this is the saga:
export function * loadData({payload}) {
try {
const resp = yield call(request, URL, Object.assign({}, buildHeaders(), {method: 'get'}));
yield put(dataFetched(resp.uuid, resp.data));
} catch (error) {
// handle the error
}
}
...
export default function * defaultSaga () {
...
yield takeLatest (FETCH_DATA_REQUEST, loadData);
}
I've been using React and Redux Sagas for a while, and this is the very first time it happens to me. I went into the Redux Sagas documentation and found nothing related to this.
I'm using Ubuntu 18.04.3, Chromiun, React 16.6.3, Redux Sagas 0.16.2... I wonder what am I doing wrong? What am I missing here?
I have a question regarding the use of sagas.
I have a button that when clicked, triggers a function that calls an action:
Component.js
onClickChainIdentifier = (event) => {
//action called
this.props.getChains();
//next function to be called
this.teste();
}
}
Action.js
export function getChains(){
return {
type: GET_CHAINS,
}
}
When this action is dispatched, it fires a constant GET_CHAINS, which calls a saga:
Saga.js
export function* getAllChains() {
const requestURL = process.env.PATH_API.GET_CHAINS;
try {
const response = yield call(requestGet, requestURL);
yield put(getChainsSuccess(response));
} catch (err) {
yield put(getChainsError(err));
}
}
export default function* sagasApp() {
yield [
fork( takeLatest, GET_CHAINS, getAllChains ),
]
}
I would like that after the api return (of success or error), I could call the this.teste function that is inside the component.
How do I make this happen?
Thanks in advance for your help.
You could pass a callback to your getAllChains function:
onClickChainIdentifier = (event) => {
this.props.getChains(() => {
this.teste();
});
}
export function* getAllChains(callback) {
const requestURL = process.env.PATH_API.GET_CHAINS;
try {
const response = yield call(requestGet, requestURL);
yield put(getChainsSuccess(response));
if (callback) {
callback();
}
} catch (err) {
yield put(getChainsError(err));
}
}
You can use flags in order to control when and if your components should render. This is a common solution for rendering a fallback UI (e.g: a spinner or a text) in order to wait until an async process (saga, thunk, API service etc) is finished and the component has all it needs to render itself.
Check the solution I have posted here, you can visit this CodeSandBox which shows how you can use flags in order to solve it.
As jank pointed out, you can test component's state in the lifecycle methods and call a function when some condition is true. For example leveraging jank's example:
componentDidUpdate (prevProps) {
if (this.props.pending && !prevProps.pending) {
this.props.test()
}
}
Will call test every time the pending prop is changed from false to true. The test function can have side effects like fetching from server or using some browser API. Same functionality can be achieved using the newer useEffect of the Hooks API.
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)
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.