Difference Between a Redux middleware and reducer accepting all actions - javascript

In my redux project, I want to check something ( for example network connection ) in every action dispatch. Should I implement using a reducer which accepts all type of actions( without type checking ) as given below
export default (state = defaultState) => ({
...state,
neworkStatus: navigator.onLine
})
or with a middleware.
const NetworkMiddleware = store => next => (action) => {
const result = next(action)
const state = store.getState()
if (navigator.onLine && !state.NetworkDetector.networkStatus) next({ type: 'NETWORK_SUCCESS' })
if (!navigator.onLine && state.NetworkDetector.networkStatus) next({ type: 'NETWORK_ERROR' })
return result
}
export default NetworkMiddleware;
what is the difference between these two implementations

It provides a third-party extension point between dispatching an
action, and the moment it reaches the reducer. People use Redux
middleware for logging, crash reporting, talking to an asynchronous
API, routing, and more.
I think it would be better to use a middleware to analyse network activity. Read these Redux docs for further information.

A middleware in redux intercepts actions and performs some specific activity before it goes to the reducer to update the state. Middleware is meant to perform such actions without making the changes to the state in store. If you perform such tracking or modification by writing a reducer, you end up maintaining a state in the store for this activity which may have nothing to do with your component update or re-rendering. This is not a good practice I suppose and doesn't go as per the framework design. So it is better to achieve it via use of a middleware.

Related

Do Vuex actions have to modify or use the store state?

Is it good practice to use Vuex store actions to perform related asynchronous operations (e.g., GET requests) without actually modifying the state of the store?
I have a Vuex store. Let's call it Host.
It contains an object as its state, with some getters to retrieve various forms of the state as well as some mutations to modify said state.
However, when it comes to actions, I perform certain asynchronous requests on host objects which I pass in to the actions as a parameter. For instance, a Host can be enabled or disabled.
I, therefore, have an action hostEnable(host), which calls an Axios GET request which responds only with an OK (200).
const getDefaultState = () => {
return {
host: {...}
}
};
export const state = getDefaultState();
const getters = {
getHost: (state) => {
return state.host;
},
...
};
const mutations = {
setHost: (state, host) => {
state.host = host;
},
...
};
const actions = {
fetchHost ({commit}, hostId) => {
api.get(hostId)
.then(({data}) => {
commit(setHost, data);
})
.catch(error => {
throw new Error(error);
});
},
createHost: ({state}) => {
return api.create(state.host);
},
hostEnable: (context, host) => {
return api.enableHost(host);
},
...
};
export default {
state,
getters,
actions,
mutations
};
Is it fine to use a Vuex store in this way, or do all actions have to either use or modify the store state?
Is it fine to use a Vuex store in this way, or do all actions have to
either use or modify the store state?
In this scenario, yes, it's perfectly fine and no, it doesn't have to modify anything.
Even though, it's not gonna behave in the way that a Vuex action is intended to work (since technically, actions are supposed to work with mutations in some fashion), you can define hostEnable as an action because it makes more sense to group all Host related business logic in one single module rather than putting it somewhere else in your codebase.
So yeah, you can use it to perform asynchronous operations without committing any mutations to your store data since it's also responsible for containing complicated business logic in your application.
Lastly, using actions for asynchronous logic is one of the high-level principles when structuring your Vue application.
Application-level state is centralized in the store.
The only way to mutate the state is by committing mutations, which are
synchronous transactions.
Asynchronous logic should be encapsulated in, and can be composed with
actions.
Key concepts:
State are to store your data.
Mutations are to handle sync operations to you data.
Actions are to handle async operations (that's why you receive a context object instead state as params )
Getters are to get data and mutate it ( i.e. get the host that contains ip from canada, and so on )
Actions are supposed to change/mutate the state only,
while Getters return data based on the state without mutating the state. See https://github.com/vuejs/vuex/issues/46#issuecomment-174539828
Store actions are for business logic without return statements. For instance, someStore.createXXX may make API call, then update list of items(state) with the response of that call and display notifications.
For just API calls we have API layer classes/functions e.g. productApi.js, userApi.js.
As a bonus, I recommend to have multiple appropriate stores(e.g. blog post store, comment store, author store) instead of having one huge global store with unrelated stuff in it.

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.

Redux: turn middleware on and off

I am looking for a way to turn a middleware on and off. I introduced a tutorial functionality - I listen to what the user is doing with the UI by checking each action with a "guidance" middleware. if the user clicks on the right place he moves to the next step in the tutorial. However this behaviour is only needed when the tutorial mode is on. Any ideas?
const store = createStore(holoApp, compose(applyMiddleware(timestamp, ReduxThunk, autosave, guidance),
window.devToolsExtension ? window.devToolsExtension() : f => f));
for now my solution was to keep the "on" switch in a guidanceState reducer and dirty check it in the middleware:
const guidance = store => next => action => {
let result = next(action)
const state = store.getState();
const { guidanceState } = state;
const { on } = guidanceState;
if (on) {
....
However, ~95% of the time the tutorial mode would be off so dirty checking every action all the time feels a bit, well, dirty... ;) Any other ways?
Don't do stateful things in middleware (unless you have a good pattern for managing that state, like Sagas). Don't do stateful things with your middleware stack at all if you can avoid it. (If you must do so, #TimoSta's solution is the correct one).
Instead, manage your tours with a reducer:
const finalReducer = combineReducers({
// Your other reducers
tourState: tourReducer
});
function tourReducer(state = initalTourState, action) {
switch(action.type) {
case TOUR_LAST_STEP:
return /* compose next tour step state here */;
case TOUR_NEXT_STEP:
return /* compose last tour step state here */;
case TOUR_CLOSE:
return undefined; // Nothing to do in this case
default:
return state;
}
}
Then, in your application use the current state of tourState to move the highlighting, and if there is nothing in tourState, turn the tour off.
store.subscribe(() => {
const state = store.getState();
if (state.tourState) {
tourManager.setTourStep(state.tourState);
} else {
tourManager.close();
}
});
You don't have to use a stateful tour manager either - if you're using React it could just be a component that pulls out tourState with a connect wrapper and renders null if there is no state:
// waves hands vigorously
const TourComponent = (props) => {
if (props.currentStep) return <TourStep ...props.currentStep />;
return null;
}
I don't know of any way to replace middlewares on the fly via redux's API.
Instead, you could create a completely new store with the old store's state as initial state and the new set of middlewares. This may work seamlessly with your application.
Three ideas you could consider:
Have the middleware listen for "GUIDANCE_START" and "GUIDANCE_STOP" actions. When those come through, update some behavior, and don't actually pass them to next.
You could write a middleware that constructs its own middleware pipeline internally, and dynamically adds and removes the guidance middleware as needed (somewhat related discussion at replaceMiddleware feature for use with lazy-loaded modules)
This might be a good use case for something like a saga, rather than a middleware. I know I've seen discussions of using sagas for onboarding workflows, such as the Key&Pad app (source:key-and-pad)

How to make thunks independent from state shape to make them portable?

I've developed a smallish standalone web app with React and Redux which is hosted on its own web server. We now want to reuse/integrate most parts of this app into another React/Redux web app.
In theory this should work quite nicely because all my React components, reducers and most action creators are pure. But I have a few action creators which return thunks that depend on the app state. They may dispatch async or sync actions, but that's not the issue here.
Let's say my root reducer looks like this:
const myAppReducer = combineReducers({
foo: fooReducer,
bar: barReducer,
baz: bazReducer
});
and my most complex action creators depend on many state slices (luckily there are only a few of those):
const someAction = function () {
return (dispatch, getState) => {
const state = getState();
if (state.foo.someProp && !state.bar.anotherProp) {
dispatch(fetchSomething(state.baz.currentId);
} else {
dispatch(doSomethingSynchronous());
}
};
}
Now the problem is that my action creators expect everything to be inside the root of the state object. But if we want to integrate this app into another redux app we'll have to mount my appReducer with its own key:
// The otherAppReducer that wants to integrate my appReducer
const otherAppReducer = combineReducers({
....
myApp: myAppReducer
});
This obviously breaks my action creators that return thunks and need to read app state, because now everything is contained in the "myApp" state slice.
I did a lot of research and thinking how to properly solve this the last few days, but it seems I'm the first one trying to integrate a Redux based app into another Redux based app.
A few hacks/ideas that came to mind so far:
Create my own thunk type so I can do instanceof checks in a custom thunk middleware and make it pass my thunks a custom getState function which will then return the correct state slice.
Mount my root reducer with it's own key and make my thunks depend on that key.
So far I think the best approach would be to create my own custom middleware, but I'm not really happy with the fact that other apps will now depend on my middleware and custom thunk type. I think there must be a more generic approach.
Any ideas/suggestions? How would you solve this kind of problem?
Have you considered not depending on store.getState()? I would decouple the actions from the application state altogether and take in the data you need from where the actions are called.
So for example:
const someAction = function (someProp, anotherProp, currentId) {
return dispatch => {
if (someProp && !anotherProp) {
dispatch(fetchSomething(currentId);
} else {
dispatch(doSomethingSynchronous());
}
};
}
This makes the actions totally reusable, with the downside of you having to now have that information elsewhere. Where else? If convenient, inside your component using this.context.store, or via props with connect, or maybe better, by having wrapper actions for your specific applications, so:
const someApplicationAction = () => {
return (dispatch, getState) => {
const { foo, bar, baz } = getState();
dispatch(someGenericAction(foo.someProp, bar.anotherProp, baz.currentID));
};
}

Where to set cookie in Isomorphic Redux Application?

I have 3 general questions about redux and isomorphic application:
What is the best way to share 'runtime' data between client and server?
For instance, when the user logged in a distant API, I store the session object in cookies. In that way, next time the client requests my front-end, the front-end server can read the cookies and initialize the redux store with it's previous session. The downside of this is that the client HAS to validate/invalidate the session on boot (eg in componentDidMount of the root component).
Should I request the session server side rather than read it from cookies?
Where should I execute the operation of cookie storing, in action creators or in reducers? Should I store the cookie in my reducer that handle the user session?
Where should I execute the operation of redirect the user (via react-router)? I mean when my user is successfully logged in, from where should I dispatch the redirect action (from the loginActionCreator once the login promise is resolved?, somewhere else? )
Thanks in advance.
I managed to get a really neat app structure.
Here's what I found for each questions:
I only share between my client and front-end server the API server token via cookies. Each time the client request the site. The front-end server calls the API server to validate the session. If these servers are on the same network it's really fast (< 5ms). I also prefetch some useful data for the client on the server before the initial render. I manage to get my application loaded and ready (javascript loaded) in the client in 600ms. It is pretty decent.
The action of storing the cookie is in my actions creators. As Ethan Clark said, we must keep reducers pure. It's much more easier to test.
I still dispatch the redirect in my signin creator once the user is authenticated. I guess it's easier to test than to dispatch the action after the promise resolution in component or elsewhere.
In fact, keeping this in mind allows us to have an app really easy to test (expect for the actions creator where you must have ton of spies).
Hope it will help someone.
Thanks for participating.
Question 2: you should execute cookie storing in your action creator. Reducers must remain pure functions.
I'm really sorry that I don't know the answers to 1 & 3, but I hope that this helps!
You should probably break your questions up into three different stack overflow questions since they're all a little different.
I agree with Ethan, your reducers should be pure with no side effects. That's the goal (aka best practice) anyway. However, Ben Nadel has been exploring questions along these lines and suggests creating a workflow layer to manage business logic rather than placing that burden on the store. You should check out his Managing Locally Cached Data with Redux in AngularJS article for more information about that.
Cookies are synchronous - you can either hydrate and subscribe to your store or make a meta reducer which wraps around the reducer before it is added to createStore. Here's quick example of both below:
//first option
const Cookie = require('js-cookie');
const loadState = (key) => Cookie.getJSON(key);
const saveState = (nextState, key) => Cookie.set(key, nextState);
const persistedState = loadState('todos');
const store = createStore(
todoApp,
persistedState
);
store.subscribe(throttle(() => {
saveState({
todos: store.getState().todos,
}, 'todos');
}, 1000));
//second option - meta reducer
// usage
const Cookie = require('js-cookie');
export function cookieMeta (
key: string,
reducer: any,
expiry: Date | number = 365,
path: string = '/',
domain: string = window.location.hostname): Function {
return function(state: any, action: any): any {
let nextState = reducer(state, action);
let cookieState = Cookie.getJSON(key);
if (action.type.includes('DELETE')) {
Cookie.remove(key);
} else if (!nextState && cookieState || action.type === '##redux/INIT') {
nextState = cookieState;
} else if (nextState && nextState !== cookieState) {
Cookie.set(key, nextState, { expires: expiry, path: path, domain: domain, secure: process.env.local });
}
return nextState;
};
};
// how to implement the meta reducer
import { todos } from './todos';
import { cookieMeta } from './middleware/cookieMeta';
export function TODOS_REDUCER (state: any, action: any) {
return cookieMeta('todos', todos)(state, action);
}
export const todoApp = combineReducers({ todos: TODOS_REDUCER })

Categories