pete
pete

Reputation: 25081

What am I missing with Redux-Saga?

I've been trying to integrate Redux-Saga into my React/Redux app for a while now and there is some key part of this that I'm missing (as it doesn't want to work).

My understanding of how this is supposed to work is as follows:

  1. In the rootSaga function, I fork off a "Watcher" which watches for a particular action to occur.
  2. When the watcher sees the action, it calls the handler for that action and calls the appropriate success/fail action when completed.
  3. I should use put or call to dispatch an action for the watcher to see.

Below is the closest I've come (conceptually) to getting it to work (code hastily anonymized, so please forgive any typos).

My sagas.ts file:

import { all, call, fork, put, takeLatest} from 'redux-saga/effects';
import { fromJS } from 'immutable';
import AppRedux, { IAction } from 'app/containers/App/reducers';

const fetchData = async (id: string, name: string): Promise<any> => {
    console.log('calling fetchData');
    const resource = `someurl?id=${id}`;
    const data = await((await fetch(resource)).json()); // request(resource);
    console.log('returning fetchData');
    return fromJS({
        id: id,
        name,
        data,
    });
};

const callFetchData = function* callFetchData(action: IAction) {
    console.log('calling callFetchData*');
    try {
        const result = yield call(fetchData, action.id, action.name);
        yield put({
            type: AppRedux.Action.DATA_FETCHED,
            result,
        });
    } catch (error) {
        yield put({
            type: AppRedux.Action.FETCH_FAILED,
            error,
        });
    }
    console.log('exiting callFetchData*');
};

const watchCallFetchData = function* watchCallFetchData(action: IAction): IterableIterator<any> {
    console.log('calling watchCallFetchData*');
    yield* takeLatest(AppRedux.Action.FETCH_DATA, callFetchData, action)[Symbol.iterator];
    console.log('exiting watchCallFetchData*');
};

export function* rootSaga(action: IAction): IterableIterator<any> {
    console.log('calling rootSaga*');

    const watcher = yield all([
        fork(watchCallFetchData, action),
    ]);

    console.log('exiting rootSaga*');
}

export default [
    rootSaga,
];

My routes.ts file:

import { RouterState } from 'react-router';
import { ComponentCallback, errorLoading, loadModule } from 'app/routes';
import AppRedux from 'app/containers/App/reducers';
import { call, put } from 'redux-saga/effects';

path: '/somepath/:id',
async getComponent(nextState: RouterState, cb: ComponentCallback) {
    try {
        const renderRoute = loadModule(cb);
        const [reducer, sagas, component] = await importModules();

        const id = nextState.params.id;
        const name = '';
        const action = {
            id,
            name,
            type: AppRedux.Action.FETCH_DATA,
        };

        console.log('routes.ts pre-put fetch_data');
        const putResult = put(action);
        console.log('routes.ts post-put fetch_data');

        return renderRoute(component);
    } catch (err) {
        errorLoading(cb)(err);
    }
},

My app.tsx file:

import * as React from 'react';
import * as ReactRouter from 'react-router';
import { connect, DispatchProp } from 'react-redux';
import AppRedux from 'app/containers/App/reducers';
import { createStructuredSelector } from 'reselect';
import { selectid, selectResponses, selectname } from 'app/containers/App/selectors';
import { ISection } from './data';


export class App extends React.Component<IProps, {}> {
    constructor(props: Readonly<IProps>) {
        super(props);
        console.log('app-constructor');
    }

    public render() {
        console.log('app-render');
        return (<div className="app" />);
    }
}

export default connect<{}, {}, IProps>(
    createStructuredSelector({
        id: selectId(),
        data: selectData(),
        name: selectName(),
    }),
    (dispatch) => ({
        fetchData: (id: string, name: string) => dispatch(AppRedux.fetchData(id, name)),
        dispatch,
    }),
)(App);

Here is the output from the console.log calls:

calling rootSaga*
calling watchCallFetchData*
exiting watchCallFetchData*
exiting rootSaga*
routes.ts pre-put fetch_data
routes.ts post-put fetch_data
app-constructor
app-render

EDIT: Clarification

What I expect to happen:

  1. I expect put(action) to dispatch the saga action and for Redux-Saga to actually do something.
  2. I expect the function callFetchData to be called, and like-wise, for it to call fetchData.
  3. I expect both of those calls to occur before I see exiting watchCallFetchData* in the console.log.
  4. I expect (at some point) to have an "A-ha!" moment.

What's actually happening:

  1. The rootSaga* function gets called.
  2. The watchCallFetchData* gets called twice? (Assumption based on what the yield statements are supposed to do).
  3. The rootSaga* function gets called again. (Assumption based on what the yield statements are supposed to do).
  4. An "A-ha!" moment continues to elude me.

Upvotes: 0

Views: 4264

Answers (2)

Ali Saeed
Ali Saeed

Reputation: 1569

I can see 2 issues:

1. Use yield instead of yield*: You should be using yield without the * (superstar) in:

yield* takeLatest(AppRedux.Action.FETCH_DATA,...

yield* is a spread operator that simply takes all the calls inside a subroutine and expands them into the macro-function yield* docs. It's not used with takeLatest.

2. Don't pass action as parameter: The action parameter is passed automatically, so it should just be:

yield* takeLatest(AppRedux.Action.FETCH_DATA, callFetchData)

As action is not passed to rootSaga() the argument would be undefined. When takeLatest will try to automatically pass the action parameter to callFetchData, it will append it to already existing list of arguments (see the Notes section in takeLatest docs) & then callFetchData will get the real action as the 2nd parameter, which the function isn't expecting, so it won't get passed.

A suggestion: yield all() internally runs all calls in parallel, so we should use call instead of fork inside it:

const watcher = yield all([
    call(watchCallFetchData, action),
]);

Difference between takeLatest() & while(take()): the take in while loop is synchronous and blocking, so it doesn't move to the next line until it sees that a desired action is dispatched. It pauses the whole saga. Whereas, takeLatest() forks an asynchronous non-blocking thread to wait for the desired action, & keeps executing the next lines of the saga.

Upvotes: 2

Cory Danielson
Cory Danielson

Reputation: 14501

The changes suggested by @Ali Saeed look correct to me, but I believe that the main issue is your usage of put inside of routes.ts.

Put only works inside of a generator (saga) run by redux-saga.

In your routes.ts file

const putResult = put(action);

That line does nothing. If you look at the return value from that you'll see that it simply returns an object.

You want to do

store.dispatch(action);

Based on your code, the store does not seem available, so you'll have to update that too.

Upvotes: 0

Related Questions