Where to put context specific multi-step async logic in Redux - javascript

tl;dr I would like to know where to place context specific multi-step async callback logic in a redux architecture, and if I am on the right track with the example code I supply below. By "multi-step" and "context specific" I typically mean server calls initiated by some user action (onClicks, etc) where the logic might only be relevant for a given component (such as redirect to a given route when successful).
The redux docs has this to say on code with side effects:
In general, Redux suggests that code with side effects should be part of the action creation process. While that logic can be performed inside of a UI component, it generally makes sense to extract that logic into a reusable function so that the same logic can be called from multiple places—in other words, an action creator function.
While that seems fine, I am not totally sure whether it is "correct" to put calls to my routing component in there, as these action creators usually seem quite generic, and triggering routing to some other resource in the app is usually quite context dependant.
I also find it a bit weird to put these quite-different beasts, that trigger action creators asynchronously and dispatch the resulting actions, in the same files (foo-model/actions.js) as the "clean" sync action creators. Is this the right place? When reading tutorials on Redux it seems like they live side by side.
The example code is quite simple and basically describes these steps:
On a user click, call a function with some param
This function calls another async function (such as a network call)
When the async call completes, trigger a routing action to another page
Background: I want to gradually refactoring a Meteor project by moving all Meteor specific bits out of the React components, eventually substituting Meteor in the front and back for something else. As there are about 50KLOC I cannot do this in one go, so I am gradually working my way through one route at a time, hoping to end up with a standard React+Redux+ReduxRouter package. In the current code routing, data fetching, and rendering is somewhat intertwined in each component, and I am having some trouble finding out where to put multi-step async logic, such as the example below.
Details on the stack I am trying to wrangle my way out of:
FlowRouter for routing
Meteor/MiniMongo for data mutation and retrieval
React Komposer for Higher Order Components
old Meteor code in MyContainerComponent
// triggered as onClick={(e) => this.saveEncounter(e.target.value)}
// in render()
const saveEncounter = (encounter) => {
Meteor.call('createEncounter', encounter, handleSaveResult);
}
};
const handleSaveResult = (err, encounterId) => {
if (err) {
this.setState({errorMessages: err});
} else {
// route to another page
NavigationActions.goTo('encounter', {encounterId: this.props.encounter._id || encounterId});
}
}
new redux code - moved into actions.js
I am trying to keep the implementation straight forward (no additional deps) at this point to understand the foundations. "Simplification" using redux-thunk, redux-actions or redux-saga need to come later. Modeled after the example code in the Redux tutorial for Async Actions
export const saveEncounter = (encounter) => {
function handleSave(err, encounterId) {
if (err) {
dispatch(createEncounterFailure(err), encounter);
} else {
dispatch(createEncounterSuccess(encounterId));
}
}
dispatch(createEncounterRequest(encounter));
Meteor.call('createEncounter', encounter, handleSave);
}
// simple sync actions creators
export const CREATE_ENCOUNTER_REQUEST = 'CREATE_ENCOUNTER_REQUEST';
function createEncounterRequest(encounter) {
return {
type: CREATE_ENCOUNTER_REQUEST,
encounter
};
}
export const CREATE_ENCOUNTER_FAILURE = 'CREATE_ENCOUNTER_FAILURE';
function createEncounterFailure(error, encounter) {
return {
type: CREATE_ENCOUNTER_FAILURE,
error,
encounter
};
}
export const CREATE_ENCOUNTER_SUCCESS = 'CREATE_ENCOUNTER_SUCCESS';
function createEncounterSuccess(encounterId) {
return {
type: CREATE_ENCOUNTER_SUCCESS,
encounterId
};
}

As you noted in a comment, Dan Abramov discussed a lot of the ideas behind handling async work in Redux in his answer for how to dispatch an action with a timeout. He also wrote another excellent answer in why do we need middleware for async flow in Redux?.
You might want to read through some of the other articles in the Redux Side Effects category of my React/Redux links list to get a better idea of ways to handle async logic in Redux.
In general, it sounds like you may want to make use of either "sagas" or "observables" for managing some of your async logic and workflow. There's a huge variety of Redux middlewares for async behavior out there - I summarized the major categories and most popular libraries in my blog post The Tao of Redux, Part 2 - Practice and Philosophy. There's also some interesting thoughts on a very decoupled saga-based Redux architecture in a post called Redux Saga in Action.

I see your point, you would like to have a way to divide and categorize your actions, is that right? Actions that will execute sync code, async code, logger, etc.
Personally, I use some naming convention. If I have to dispatch an action that has to fetch some data, I call it REQUEST_DATA. If have to store some data arrived from the server to the ReduxStore, I call it STORE_DATA.
I don't have a specific pattern. I also have to point that I divide my codebase based on the feature, so the modules where I define my actions are pretty small and neat

In my experience with Redux, I haven't found any problems with putting async calls inside action creators. I think redux-thunk or some other middleware is very helpful, even for a simple setup.
The only thing I'd add is that I don't find your sample code very readable.
Personally I've come to like the ducks pattern, but also just keeping action types, action creators and reducers in separate files would work to improve clarity.
Hope this helps.

Related

Design question for Redux: When you want to make a Redux action to dispatch other Redux actions, should you?

I'm slowly adding Redux to React app. Not to all state, but to state that is consumed by many different components in the application.
There was function called saveOrRemoveNewItemOrBlankItem() that could be triggered by multiple events. I'd like to make this a single Redux action, or, at least I don't want to have to repeat the same logic everywhere the actions are dispatched, for example:
if(item.isBlank){
store.dispatch(removeItem())
} else {
store.dispatch(saveItem())
}
It would be better to just call one action (albeit one with a long name):
store.dispatch(saveOrRemoveNewItemOrBlankItem())
I know it's possible (using thunk middleware) for one Redux action creator to dispatch other Redux actions, and am already doing this in places. However it isn't possible (or at least I don't know if it's possible) if I want to save time on boilerplate by using ReduxJS/Toolkit CreateSlice. I can accept more boilerplate, but I want to know... is it terribly 'un-Redux' for actions to call other actions (in this case for saveOrRemoveNewItemOrBlankItem to call either saveItem or removeItem conditionally) ? What's the proper design route to accomplish this? Surely not writing the same conditional logic in multiple places... Is there another layer that can offload this responsibility from the react components themselves? New(ish) to React and new to Redux. Less interested in solving this particular example than getting a good grasp of best practices. Thoughts, opinions, facts, expertise, brilliance appreciated... What am I missing?
It is fine to combine actions in another action, you are right to not want this logic in your component. You don't need thunk to do this when using createSlice:
const slice = createSlice(/** stuff that creates slice */);
const sliceWithCombinedAction = {
...slice,
actions: {
...slice.actions,
commitItem(item) {
return item.isBlanc
? slice.actions.removeItem()
: slice.actions.saveItem();
},
},
};
export default sliceWithCombinedAction;

Where should things like the current state of an async action be stored in a react-redux application?

I have a login popup which maps a 'isLoggingIn' boolean to the redux store. When a login request action is dispatched a saga intercepts the action and sends another action that the login is processing, the reducer will take that in and set the 'isLoggingIn' boolean to true.
My store:
export interface AppState {
playerToken:string,
loginOpen: boolean,
loginProcessing: boolean
}
The login saga:
function* loginUser(action: any) {
yield put({ type: (LOGIN + PROCESSING) });
try {
const response = yield call(apiCall, 'api/token', 'POST', { username: action.payload.username, password: action.payload.password });
if (response)
{
yield put({ type: (LOGIN + SUCCESS), payload: response.data });
}
catch ({ statusCode }) {
if (statusCode === 401) {
yield put({ type: (LOGIN + FAIL), payload: { error: "Invalid username or password" } })
}
console.log(statusCode);
}
}
Once the saga is done with the login if there's an error it dispatches an action which the reducer sets to a 'loginError' string in the store and sets the isLoggingIn to false, otherwise isLoggingIn is set to false and the user login id is set which prompts the popup to hide itself (i.e. isVisible={this.props.playerToken == undefined).
This seems insanely complicated but I'm not sure how to break this down using Redux principles. I feel strongly the isProcessingLogin should be part of the components state, but the component has no real idea what's going on after it sends the login attempt event and there's no way for it to ever know unless it's listening on for something in the props.
It gets much worse with the various crud operations which need to happen and the various 'isCreatingXModel' booleans which have to be set to true/false in the store and mapped correctly in components.
Is this how redux is supposed to work or am I over using it in places it doesn't belong?
If this is how redux is supposed to be used what are its benefits exactly? I've read online a lot about things which make sense like having a single point of truth, but they can all be done without the crazy redux bloat, I've read people say not to use redux until you need it but that means I'm going to be doing api calls in two conceptually separate areas of code when redux is integrated whenever I 'need it', finally one of the biggest advantages I see purported by advocates is its ability to rewind and move forward in time, which is great but it won't work in any live application which connects to a database in the backend it manipulates unless as part of rewinding there's an undo last api call action.
Keep in mind that these are all entirely my opinions.
1. You might not need sagas (or thunk or other 'async' redux plugin)
Remember that redux is state management only. The API calls can be written in vanilla javascript with or without redux. For example: here's a basic replication of your flow without sagas:
e.g.
import { setLoadingStatus } from './actions'
import { store } from './reducers' // this is what is returned by a createStore call
export function myApiCall(myUrl, fetchOptions) {
store.dispatch(setLoadingStatus('loading'))
return fetch(myUrl, fetchOptions)
.then((response) => {
store.dispatch(setLoadingStatus('succeeded', data))
// do stuff with response data (maybe dispatch a different action to use it?)
})
.catch((error) => {
store.dispatch(setLoadingStatus('failed', error))
// do stuff
})
}
Note the use of store.dispatch. There's an interesting notion in React-Redux that you can only dispatch actions with mapDispatchToProps, but fortunately, that's not true.
I replaced your multiple actions with one that takes a state and optional data. This'll reduce the number of actions and reducers you need to write, but your action history will be harder to read. (Instead of three distinct actions, you'll only have one.)
2. You might not need redux.
The example function above could look basically identical if you weren't using redux -- imagine replacing the store.dispatch calls with this.setState calls. In the future, when you added it, you'd still have to write all the reducer, action, action creator boilerplate, but it would be only slightly more painful than doing it from the start.
As I said above, I usually go with Redux when working with React the built-in state management is has a bad mental map with any sort of large app.
There are two opposing rules of thumb:
use redux for state that needs to be shared between components
use redux for all state and get a single source of truth
I tend to lean to the second one. I hate hunting down errant pieces of state in the leaves of a large React component tree. This is definitely a question with no "correct" answer.

React Native: using redux saga eventChannel to externalise event listeners from components

Redux saga eventChannel is a very elegant solution to connect to external events from outside your components code, keeping them cleaner and conciser. An use case could be to react to global App state changes, like application in the foreground, background, inactive...
I am worried about the unsubscribe process when it comes to React Native.
Inside components, componentWillUnmount is the place to perform your un-subscriptions, so we have some UI hook that guarantees us the success of the listener removal.
When I apply the below code for instance, to keep track of App state changes (active, inactive, or background), could I be exposed to memory leaks?
type NextAppState = 'active' | 'inactive' | 'background';
function createAppStateChannel() {
return eventChannel((emit: (nextState: NextAppState) => mixed) => {
AppState.addEventListener('change', emit);
return () => {
AppState.removeEventListener('change', emit);
};
});
}
export function* appStateListenerSaga(): Generator<*, *, *> {
try {
const appStateChannel = yield call(createAppStateChannel);
while (true) {
const nextAppState: NextAppState = yield take(appStateChannel);
if (nextAppState === 'active') {
// Do something, like syncing with server
}
} finally {
if (yield cancelled()) {
appStateChannel.close();
}
}
}
I am thinking of the case when the JS context gets killed for whatever reason.
My concern is that sagas wouldn't be cancelled (as far as I know), the un-subscription wouldn't be performed, so the native listener would remain registered. Next time the app would be reopened, we'd register a duplicated event listener and so on so forth.
Anyone could point me out whether this is safe or is it basically better to use a HOC? I am also using react-native-navigation where there are several react root views (one per screen), that's why HOC are not a good fit for that case, since I'd have to wrap every parent screen with the HOC and also if I have 3 screens pushed onto the stack, on app resume the logic would be executed 3 times.
So apparently it depends on the implementation of the native module.
If the native module just keeps one listener and all the subscriptions are managed in JS side, then it’s not an issue.
However, if new listeners are handled on native side, it’ll be an issue if the JS context gets killed since memory would be leaked.
An example of two modules that keep subscriptions on the JS side are AppState and BackHandler. Both keep one global listener on the native side and multiple subscribers on the JS side, so both can be safely used with a redux-saga eventChannel.
You can check the source code of AppState in here as an example: https://github.com/facebook/react-native/blob/master/Libraries/AppState/AppState.js
I still haven't stumbled upon a module that does the other way around.
As a rule of thumb, just glance at the module on the github source code to see how subscriptions are managed.

Why do you need 'Actions' as data in redux?

Redux documentations says I should make actions and action creators, like this:
function addTodo(filter) {
return {
type: SET_VISIBILITY_FILTER,
filter
}
}
Then write reducers, like this:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
}
}
I then invoke the action using dispatch:
store.dispatch(addTodo("Ask question on stackoverflow"));
It seems there's a one-one correspondence between actions and reducers; the sole purpose of the action is to select a reducer and provide input data for that reducer.
Why don't we skip the middle man and identify actions with reducers and action creators with functions producing reducers? Then dispatch would take a single argument, a reducer/action of type State => State:
// Action/reducer. (Parametrised state transformer, really.)
const addTodo = text => state => {
return Object.assign({}, state, {
visibilityFilter: action.filter
});
}
// Dispatch takes as a argument the action/reducer
store.dispatch(addTodo("Ask question on stackoverflow"));
You'd lose the ability to serialise actions, but otherwise, it seems you'd get rid of boilerplate action creators and express more clearly the connection between actions and reducers. If you're in Typescript, you also get typechecking of the data in actions, which is difficult to express otherwise.
So what reasons for having actions as data am I missing?
The main purpose of action in Redux is to reduce the state.
Reduce method will be called on array of actions (thats why it called a reducer). Example:
import reducer from './reducer';
const actions = [
{type: 'INIT'},
{type: 'SOME_ACTION', params: {...}},
{type: 'RECEIVE_DATA', data: [...]},
{type: 'SOME_ANOTHER_ACTION', params: {...}},
{type: 'RECEIVE_DATA', data: [...]},
...
];
const finalState = actions.reduce(reducer, undefined);
Action creators is a function that can create actions. It is not necessary that action creator will create only one action.
Actually, if your reducer is able to receive functions instead of objects - your actions will be functions and it will do the main purpose, but you can loose some benefits of Redux functional abilities.
In that case the reducer will be implemented like this:
function reducer(state, action) {
return action(state);
}
The reasons why you may create actions as {type: 'ACTION_NAME'} format:
Redux DevTools expects this format.
You need to store sequence of actions.
Reducer makes state transformations on worker.
Everybody in Redux ecosystem use this format. It's a kind of convention.
Hot reloading abilities (your stored functions will not be reloaded).
You need to send actions as is on server.
Debugging benefits - to see the stack of actions with actions names.
Writing unit tests for reducer: assert.equal(finalState, expectedState).
More declarative code - action name and parameters are about "what to do" and not about "how to do" (but addTodo('Ask question') is declarative too).
Note about coupling between action creators and state changes
Just compare two notations:
First:
function someActionCreator() {
return {
type: "ADD_TODO",
text: "Ask question on stackoverflow"
}; // returns object
}
Second:
function someActionCreator() {
return addTodo("Ask question on stackoverflow"); // returns function
}
"In both cases we see that code is declarative and action creator is decoupled from state change. You can still reuse addTodo or dispatch two addTodo's or use middleware or dispatch compose(addTodo('One'), addTodo('Two')). The main difference is that we created Object and Function and place in code where state changes.
There is NOT a one-to-one mapping between actions and reducers. Per Dan Abramov's comments at https://github.com/Pitzcarraldo/reduxible/issues/8 :
It reinforces a very common misconception about Redux: namely that action creators and reducers are one-to-one mapping.
This is only true in trivial examples, but it is extremely limiting in real applications. Beginners exposed to this pattern couple reducers and action creators, and fail to realize they're meant to be many-to-many and decoupled.
Many reducers may handle one action. One reducer may handle many actions. Putting them together negates many benefits of how Flux and Redux application scale. This leads to code bloat and unnecessary coupling. You lose the flexibility of reacting to the same action from different places, and your action creators start to act like “setters”, coupled to a specific state shape, thus coupling the components to it as well.
As for actions and the "type" parameter, the other answers are right. That's deliberately how Redux was designed, and that was intended to give the benefits of serialization for debugging purposes.
Good question.
Separating actions from state changes is really a Flux pattern, rather than a specifically Redux thing. (Though I will answer the question with reference to Redux.) It's an example of loose coupling.
In a simple app, tight coupling between actions and state changes might be fine. But in a larger app, this could be a headache. For instance, your addTodo action might trigger changes in several parts of the state. Splitting actions from state changes - the latter performed in reducers - allows you to write smaller functions, which are easier to reason about and more testable.
Additionally, decoupling your actions and state changes allows your reducer logic to be more reusable. e.g. Action X might trigger state changes A and B, whilst action Y only triggers state change A.
Furthermore, this decoupling gives rise to a Redux feature called middleware. Middleware listens to action dispatches. It doesn't change the state of the app, but it can read the current state and the next state, as well as the action information. Middleware is useful for functionality extraneous from the core application logic, e.g. logging and tracking (dev tools were mentioned in a previous answer).
[UPDATE] But why have actions as objects?
If it were simply a matter of functions calling other functions, that decoupling would be much less explicit. Indeed it may even get lost; since most actions only enact one state change, developers might tire of a single function calling a single function and do away with the separation entirely.
The other thing is the Flux data flow model. One-way data flow is very important to the Flux/React paradigm. A typical Redux/React goes something like this: Store state -> Higher order React components -> Lower order React components -> DOM. The action is the aberration in the model; it's the backwards arrow transmitting data from the view to the store. It makes sense to make the action something as loud and emphatic as possible. Not a mere function call, but a dispatched object. It's as if your app were announcing Hey! Something important happened here!

React, Flux, React-Router Dispatch Error - Possible batchUpdates solution?

Alright so I'm stuck on an issue with reactjs, flux architecture, and react-router. I have the following routes (just a portion of the routes):
<Route name="prepare-seniors">
<Route name="senior" path=":candidateId" handler={SeniorCandidate}>
<DefaultRoute handler={SeniorProfile}/>
<Route name="senior-recommendations" path="recommends">
<DefaultRoute handler={SeniorRecommends}/>
<Route name="senior-rec-new" path="new"/>
</Route>
</Route>
</Route>
The Senior Profile view makes an API call to load the individual's data. When you navigate to the Recommends view, the individual's id is used to make another call to load the recommendations. It works great if I actually view the profile page first and navigate to the recommendations page. But if I do a hard reload I get:
Uncaught Error: Invariant Violation: Dispatch.dispatch(...): Cannot dispatch in the middle of a dispatch.
I realize this is because the dispatch is getting called after the first API returns, which goes out and starts updating components. Before it finishes the recommendations page calls its API and tries to dispatch its results. I read on a forum that React.addons.batchUpdates is a way to fix this, but I couldn't figure out how to get it to work. GitHub Batch Updates Issue and another link here that discusses something similar Trying to use waitFor. The first one recommends adjusting the dispatcher by adding the following:
// assuming a var `flux` containing your Flux instance...
var oldDispatch = flux.dispatcher.dispatch.bind(flux.dispatcher);
flux.dispatcher.dispatch = function(action) {
React.addons.batchedUpdates(function() {
oldDispatch(action);
});
};
But I could not make this work. Maybe I'm just implementing it incorrectly.
I've read the Chat and TodoMVC examples. I understand how the waitFor is used in the chat example...but those both use the same API so it's clear that the one is going to wait for the other. My issue involves a race condition between APIs and dispatch...and I don't think setTimeout is a good solution.
What I need is to see how to set up the dispatcher in such a way that it will queue the dispatch or API calls. Or a better way to tell each component to make an API call for it's data, so I don't even have a dispatch issue.
Oh so here's my Dispatcher.js file so you can see how it's set up:
'use strict';
var flux = require('flux'),
Dispatcher = require('flux').Dispatcher,
React = require('react'),
PayloadSources = require('../constants/PayloadSources'),
assign = require('object-assign');
//var oldDispatcher = flux.Dispatcher.dispatch.bind(AppDispatcher);
//
//flux.dispatcher.dispatch = function(action) {
// React.addons.batchedUpdates(function() {
// oldDispatcher(action);
// });
//};
var AppDispatcher = assign(new Dispatcher(), {
handleServerAction: function(action) {
var payload = {
source: PayloadSources.SERVER_ACTION,
action: action
};
this.dispatch(payload);
},
handleViewAction: function(action) {
if (!action.type) {
throw new Error('Empty action.type: you likely mistyped the action.');
}
var payload = {
source: PayloadSources.VIEW_ACTION,
action: action
};
this.dispatch(payload);
}
});
module.exports = AppDispatcher;
Ok first I'm just going to say I am learning React and Flux at the moment and am by no means an expert. However I'm going to give it a shot anyway:
From what you have said, it sounds like you have 2 asynchronous operations triggering off and then when they return trying to send dispatch messages The issue arising from the dispatch call being triggered twice from 2 independent aync webservice calls.
I don't think batching the updates would help in this case as it would depend heavily on timing(ie. if it has queued the re-render and is waiting for the opportunity to execute when the 2nd call comes in it could batch successfully, however if it comes in mid-update you are in the exactly the same situation place you are now).
I think the actual issue here is coming from how/when these Actions are being triggered(you mention this briefly at the end of your post). It sounds like you are triggering Actions from the React component Render calls(kind of like lazy loading). This is pretty much exactly the thing Flux is trying to avoid, as that is a pattern that could easily result in an infinite loop or weird states/patterns in the flow of the application.
As you mentioned in your description you probably need to trigger this loading up front instead of from the individual components, from the little I know I would suggest that needs to be done prior to calling React.render on your core component. If you look in the example chat app they do a similar thing in app.js:
ChatExampleData.init(); // load example data into localstorage
ChatWebAPIUtils.getAllMessages();
React.render(
<ChatApp />,
document.getElementById('react')
);
So I would suggest following the above pattern, loading any initial data up front while showing a loading spinner to the client.
Alternatively, depending on your tech stack you could render initially on the server (node example here .Net version at ReactJS.Net) so you don't have that delay while the client side boots up(also you avoid all the funny business with search engine indexing if that is a concern (BTW I haven't tried either of these, just read about them).

Categories