Best practice for updating individual state properties with Redux Saga

Refresh

4 weeks ago

Views

825 time

1

So I'm working on implementing an application in React with Redux Saga and I'm kind of baffled at how little information there is out there for my particular use case, as it doesn't seem that strange. Quite possibly I am using the wrong terms or thinking about the problem in the wrong way, as I am rather new to React/Redux. In any event, I have been stymied by all my attempts to google this issue and would appreciate some insight from someone more experienced in the framework than I am.

My application state has a userSettings property on it which manages a few configuration options for the logged in user. At one point in the application, a user can flip a switch to disable the display of an "at a glance" dashboard widget, and I need to pass this information off to a backend API to update their settings info in the database, and then update the state according to whether this backend update was successful.

My code as it stands currently has a main saga for all user settings updates, which I intend to reach via a more specific saga for this setting in particular, thus:

Dashboard.js

function mapStateToProps(state) {
    const { userSettings } = state;
    return { userSettings };
}
...
class Dashboard extends Component {
    ...
    ...
    hasDashboardAtAGlanceHiddenToggle() {
        const { dispatch, userSettings } = this.props;
        dispatch(setHasDashboardAtAGlanceHidden(!userSettings.hasDashboardAtAGlanceHidden));
    }
}
export default connect(mapStateToProps)(Dashboard);

updateUserSettingsSaga.js

import { take, put, call } from 'redux-saga/effects';
import axios from 'axios';

import {
    UPDATE_USER_SETTINGS,
    SET_HAS_DASHBOARD_AT_A_GLANCE_HIDDEN,
    updateUserSettings,
    updatedUserSettingsSuccess
} from '../../actions';

export function* setHasDashboardAtAGlanceHiddenSaga() {
    const action = yield take(SET_HAS_DASHBOARD_AT_A_GLANCE_HIDDEN);
    const newValue = action.data;
    //QUESTION HERE -- how to get full object to pass to updateUserSettings
    yield put(updateUserSettings(stateObjectWithNewValuePopulated));
}

export default function* updateUserSettingsSaga(data) {
    yield take(UPDATE_USER_SETTINGS);
    try {
        const response = yield call(axios.put, 'http://localhost:3001/settings', data);
        yield put(updatedUserSettingsSuccess(response.data));
    } catch (e) {
        yield put(updatedUserSettingsFailure());
    }
}

My question, as noted in the code, is that I'm not sure where/how the logic to merge the updated value into the state should occur. As near as I can figure, I have three options:

  1. Build the updated state in the component before dispatching the initial action, ie:

    hasDashboardAtAGlanceHiddenToggle() {
        const { dispatch, userSettings } = this.props;
        const newState = Object.assign({}, userSettings , {
            hasDashboardAtAGlanceHidden: !userSettings.hasDashboardAtAGlanceHidden
        });
        dispatch(setHasDashboardAtAGlanceHidden(userSettings));
    }
    

    }

  2. Use redux-saga's select effect and build the full state object in the more specific initial saga, ie:

    export function* setHasDashboardAtAGlanceHiddenSaga() {
        const action = yield take(SET_HAS_DASHBOARD_AT_A_GLANCE_HIDDEN);
        const newValue = action.data;
        const existingState = select(state => state.userSettings);
        const updatedState = Object.assign({}, existingState, {
            hasDashboardAtAGlanceHidden: newValue
        });
        yield put(updateUserSettings(updatedState));
    }
    
  3. Retrieve the server's copy of the user settings object before updating it, ie:

    export default function* updateUserSettingsSaga() {
        const action = yield take(UPDATE_USER_SETTINGS);
        try {
            const current = yield call(axios.get, 'http://localhost:3001/settings');
            const newState = Object.assign({}, current.data, action.data);
            const response = yield call(axios.put, 'http://localhost:3001/settings', newState);
            yield put(updatedUserSettingsSuccess(response.data));
        } catch (e) {
            yield put(updatedUserSettingsFailure());
        }
    }
    

All of these will (I think) work as options, but I'm not at all clear on which would be the idiomatic/accepted/preferable approach within the context of Redux Saga, and there is a bewildering lack of examples (at least that I've been able to find) featuring POST/PUT instead of GET when interfacing with outside APIs. Any help or guidance would be appreciated -- even if it's just that I'm thinking about this in the wrong way. :D

1 answers

3

The GET/PUT/POST aspect isn't relevant to the question. Overall, your question really comes down to the frequently asked question "How do I split logic between action creators and reducers?". Quoting that answer:

There's no single clear answer to exactly what pieces of logic should go in a reducer or an action creator. Some developers prefer to have “fat” action creators, with “thin” reducers that simply take the data in an action and blindly merge it into the corresponding state. Others try to emphasize keeping actions as small as possible, and minimize the usage of getState() in an action creator. (For purposes of this question, other async approaches such as sagas and observables fall in the "action creator" category.)

There are some potential benefits from putting more logic into your reducers. It's likely that the action types would be more semantic and more meaningful (such as "USER_UPDATED" instead of "SET_STATE"). In addition, having more logic in reducers means that more functionality will be affected by time travel debugging.

This comment sums up the dichotomy nicely:

Now, the problem is what to put in the action creator and what in the reducer, the choice between fat and thin action objects. If you put all the logic in the action creator, you end up with fat action objects that basically declare the updates to the state. Reducers become pure, dumb, add-this, remove that, update these functions. They will be easy to compose. But not much of your business logic will be there. If you put more logic in the reducer, you end up with nice, thin action objects, most of your data logic in one place, but your reducers are harder to compose since you might need info from other branches. You end up with large reducers or reducers that take additional arguments from higher up in the state.

I also wrote my own thoughts on "thick and thin reducers" a while back.

So, ultimately it's a matter of how you prefer to structure the logic.