1

I'm working on an app in React with Redux, Saga, and using Typescript.

The app structure is setup such that each primary top level container component has a corresponding file in a Store directory for defining it's action creators, reducers, and sagas.

When the app starts all of the reducers are combined from the Store files, and the Sagas are combined into a common rootSaga function.

Everything works fine except now that I am trying to use a selector to load some state properties into one of my Sagas. I am not getting any errors but my selector function isn't returning my state values.

If I try to use the getState() function in my Store file I do get a Typescript error 'Cannot find the name getState'.

Clearly I'm not including the correct library in my Store file or I'm not calling the state function by the correct namespace but I can't figure out what's wrong.

I switched from Thunk middleware to using Saga. When Thunk was wired in to the app I was able to use getState in the Store file.

This is the Store file with my action creators, reducers, and sagas.

My selector function is in the file as well (export const getVersionQueueFilters):

import { fetch, addTask } from 'domain-task';
import { Action, Reducer, ActionCreator } from 'redux';
import { takeLatest, takeEvery } from "redux-saga"
import { call, put, take, race, select } from "redux-saga/effects"
import * as moment from 'moment';

// -----------------
// STATE - This defines the type of data maintained in the Redux store.

export interface ASVersionQueueState {
    queuedVersions: QueuedVersion[];
    versionQueueFilter: VersionQueueFilter;
    eventsFilterList: SelectListItem[];
    employeesFilterList: SelectListItem[];
    gridIsLoading: boolean;
    versionQueueRefresh: boolean;
    error: boolean;
}

export interface QueuedVersion {
    VersionCode: string;
    VersionQualifier: string;
    VersionID: string;
    ProductID: string;
    PieceName: string;
    PrintClass: string;
    FirstInhomeDate: string;
    AccountID: string;
    AccountExecutive: string;
    AccountManager: string;
    ArtManager: string;
    AdUID: string;
    Status: string;
    Queue: string;
    DueDateOverride: string;
    IsLocked: string;
}

export interface VersionQueueFilter {
    StartDate: string;
    EndDate: string;
    PieceType: Array<string>;
    EventType: Array<string>;
    EventID: string;
    Employee: string;
}

export interface SelectListItem {
    OptionName: string;
    OptionVal: string;
}

export let DefaultVersionQueueFilter = {
    StartDate: moment().subtract(30, 'days').format('YYYY-MM-DD'),
    EndDate: moment().format('YYYY-MM-DD'),
    PieceType: ['impactpc'],
    EventType: ['special'],
    EventID: '',
    Employee: '12345'
}

// Version Queue polling delay value
let versionQueuePollDelay: number = 10000;  // Delay in milliseconds

// -----------------
// ACTIONS - These are serializable (hence replayable) descriptions of state transitions.
// They do not themselves have any side-effects; they just describe something that is going to happen.
// Use @typeName and isActionType for type detection that works even after serialization/deserialization.

interface PollVersionsAction {
    type: 'POLL_VERSIONS';
    versionQueueFilter: VersionQueueFilter;
    versionQueueRefresh: boolean;
}

interface PollRequestVersionsAction {
    type: 'POLL_REQUEST_VERSIONS';
    versionQueueFilter: VersionQueueFilter;
    versionQueueRefresh: boolean;
}

interface PollRequestVersionsSuccessAction {
    type: 'POLL_REQUEST_VERSIONS_SUCCESS';
    versionQueueFilter: VersionQueueFilter;
    receivedVersions: QueuedVersion[];
    versionQueueRefresh: boolean;
}

interface PollRequestVersionsErrorAction {
    type: 'POLL_REQUEST_VERSIONS_ERROR';
}


// Declare a 'discriminated union' type. This guarantees that all references to 'type' properties contain one of the
// declared type strings (and not any other arbitrary string).
type KnownAction = PollVersionsAction | PollRequestVersionsSuccessAction | PollRequestVersionsErrorAction;

// ----------------
// ACTION CREATORS - These are functions exposed to UI components that will trigger a state transition.
// They don't directly mutate state

export const actionCreators = {

    pollVersions: () => {
        return { type: 'POLL_VERSIONS', versionQueueFilter: getVersionQueueFilters, versionQueueRefresh: true }
    },
    pollRequestVersions: (versionQueueFilter: VersionQueueFilter, versionQueueRefresh: boolean) => {
        return { type: 'POLL_REQUEST_VERSIONS', versionQueueFilter: versionQueueFilter, versionQueueRefresh: versionQueueRefresh }
    },
    pollRequestVersionsSuccess: (versionQueueFilter: VersionQueueFilter, versionQueueRefresh: boolean, data: QueuedVersion[]) => {
        return { type: 'POLL_REQUEST_VERSIONS_SUCCESS', versionQueueFilter: versionQueueFilter, receivedVersions: data, versionQueueRefresh: versionQueueRefresh }
    },
    pollRequestVersionsError: () => {
        return { type: 'POLL_REQUEST_VERSIONS_ERROR' }
    }
};

// ----------------
// REDUCER - For a given state and action, returns the new state. To support time travel, this must not mutate the old state.
const unloadedState: ASVersionQueueState = { gridIsLoading: false, versionQueueRefresh: false, queuedVersions: [], versionQueueFilter: DefaultVersionQueueFilter, eventsFilterList: [], employeesFilterList: [], error: false };

export const reducer: Reducer<ASVersionQueueState> = (state: ASVersionQueueState, incomingAction: Action) => {
    const action = incomingAction as KnownAction;
    switch (action.type) {
        case 'POLL_VERSIONS':
            return {
                ...state,
                versionQueueFilter: action.versionQueueFilter,
                versionQueueRefresh: action.versionQueueRefresh,
                gridIsLoading: true
            }
        case 'POLL_REQUEST_VERSIONS_SUCCESS':
            // Only accept the incoming data if it matches the most recent request. This ensures we correctly
            // handle out-of-order responses.
            if (action.versionQueueFilter === state.versionQueueFilter && action.versionQueueRefresh === state.versionQueueRefresh) {
                return {
                    ...state,
                    queuedVersions: action.receivedVersions,
                    versionQueueRefresh: action.versionQueueRefresh,
                    gridIsLoading: false
                }
            }
            break;
        case 'POLL_REQUEST_VERSIONS_ERROR':
            return {
                ...state,
                error: true
            }
        default:
            // The following line guarantees that every action in the KnownAction union has been covered by a case above
            const exhaustiveCheck: never = action;
    }

    return state || unloadedState;
};

// Sagas
// Saga Watchers
export const sagas = [
    takeEvery('POLL_VERSIONS', fetchPollVersionsAsync)
]

// Selector Function
export const getVersionQueueFilters = (store: ASVersionQueueState) => store.versionQueueFilter;

// Utility function to delay effects
export function delay(delayMS: number) {
    const promise = new Promise(resolve => {
        setTimeout(() => resolve(true), delayMS)
    });
    return promise;
}

export function* versionPoller() {
    const versionQueueFilters = yield select(getVersionQueueFilters);
    try {
        yield call(delay, versionQueuePollDelay);
        yield put(actionCreators.pollVersions() );
    } catch (error) {
        // cancellation error
        return;
    }
}

export function* watchVersionPoller() {
    while (true) {
        yield take('POLL_REQUEST_VERSIONS_SUCCESS');
        yield call(versionPoller);
    }
}

export function* fetchPollVersionsAsync(action: PollVersionsAction) {
    try {
        yield put(actionCreators.pollRequestVersions(action.versionQueueFilter, action.versionQueueRefresh));
        const data = yield call(() => {
            return fetch('api/Versions')
                .then(res => res.json())
        }
        );
        yield put(actionCreators.pollRequestVersionsSuccess(action.versionQueueFilter, action.versionQueueRefresh, data));
    } catch (error) {
        yield put(actionCreators.pollRequestVersionsError());
    }
}

The selector is used in the saga function "versionPoller()".

Basically I'm polling my API for any updated data but it requires passing at least a default set of filter values. I want to use the filter values currently in state for that.

I have also tried defining my selector function as:

export const getVersionQueueFilters = getState().ASVersionQueueState.versionQueueFilter;

When I do that I get the error 'cannot find the name getState'.

Any idea what I'm doing wrong?

2 Answers 2

1

The parameter of select is a callback:

const callback = state => state.asVersionQueue.versionQueueFilter
const versionQueueFilters = yield select(callback)
Sign up to request clarification or add additional context in comments.

Comments

0

Looks like I misunderstood how the saga select works.

In my function:

export function* versionPoller() {
    const versionQueueFilters = yield select(getVersionQueueFilters);
    try {
        yield call(delay, versionQueuePollDelay);
        yield put(actionCreators.pollVersions() );
    } catch (error) {
        // cancellation error
        return;
    }
}

what I should have done was:

const state = yield select();
const versionQueueFilters = state.asVersionQueue.versionQueueFilter;

The function I was passing into my select wasn't providing anything but leaving it empty returns the entire state tree.

1 Comment

you are correct that calling select() without arguments returns the entire state tree, but I don't think that was the reason for the initial bug. As @dancerphil answered, your selector had a bug, where it should have accessed store.asVersionQueue.versionQueueFilter, but instead you simply accessed store.asVersionQueueFilter

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.