I need to add functionality into my react/redux application that is event based.
I'm using redux and I have reducers that change my application state on events through actions. Some actions for example: (USER_CLICKED_BUTTON, AJAX_CALL_STARTED, AJAX_CALL_ERROR, AJAX_CALL_SUCCESS)
I want to add listeners to these events and do some work that isn't state related, specifically, call another API like analytics.
Does it make sense to add another "reducer" with no real state, that simply listens to these actions and acts (for example call ga(...)?)
something like this:
var Actions = require ('../actions/actionTypes');
var gaReducer = function(state, action) {
switch (action.type){
case Actions.USER_CLICKED_BUTTON:
ga('send', 'event', 'user-clicked-button');
break;
}
return state;
}
This sounds like a good case for middleware. Check out https://github.com/evgenyrodionov/redux-logger for an example.
Then your store might look something like:
const middleware = [
myAnalyticsMiddleware()
];
export default createStore(reducer, applyMiddleware(...middleware));
Related
I am beginner in Redux and I want to use it for asynchronous logic. Redux style quide recommends to use redux-thunk for it, but it seems I don't need it if I use redux in following way:
class Actions {
constructor(dispatch) {
this.dispatch = dispatch;
}
someSyncAction1(data) {
this.dispatch({
type: SOME_SYNC_ACTION1,
payload: data,
})
}
someSyncAction2(data) {
this.dispatch({
type: SOME_SYNC_ACTION2,
payload: data,
})
}
async someAsyncAction(data1, data2) {
this.someSyncAction1(data1);
await somethingAsync();
this.someSyncAction2(data2);
}
}
// then in my react component:
function MyComponent() {
const dispatch = useDispatch();
const actions = new Actions(dispatch);
//...
return <div onClick={() => actions.someAsyncAction(1, 2)}></div>;
}
It seems to be a simple way but I worry whether it can lead to errors. Please help me to understand what is wrong with it.
This is not very different from the useActions hook referred to in the Hooks documentation - at least for the synchronous stuff.
In the async stuff, you are losing functionality though: Thunks can at any given time access the current state by calling getState.
Also, and this is probably more important: thunks are not only recommended, they are a pattern almost every redux developer knows. So they look at your code and can immediately go to work. Your pattern on the other hand is not established, so it will lead to conflicts if someone other will ever take over your code - without any real benefit.
With reduxjs-toolkit I define my store and middleware something like this:
const store = configureStore({
reducer: rootReducer,
middleware: [reduxWebsocketMiddleware, adamBrownMiddleware]
});
However I only want to apply this middleware to certain reducers, not all. Specificially I want to apply websocket middleware to certain reducers, whereas in others I'm calling an API and this websocket middleware is not necessary.
It seems like adding another store to do this is an anti-pattern.
So how can I apply middleware only for certain reducers?
You can always, in your middleware, check the action.type property (for example if it begins with "mySliceName/") and otherwise skip the rest of the middleware.
As you don't really give an example what your middleware is doing and why you want to limit it, it's not really possible to give you any more input than that.
This could look like this:
const middleware = api => next => action => {
if (!action.type.startsWith("myWebsocketSlice/") { return next(action); }
// normal middleware code here
}
In the Redux Style Guide, it is strongly recommended to Put as Much Logic as Possible in Reducers:
Wherever possible, try to put as much of the logic for calculating a
new state into the appropriate reducer, rather than in the code that
prepares and dispatches the action (like a click handler).
What I'm not sure of is, if thunks are also considered to be "the code" of some sort. Besides, we've also been (mis?)using thunks to grab data from other slices of state.
Hypothetically simplified code snippet of such thunk:
const addX = x => (dispatch, getState) => {
const { data, view } = getState();
const { y } = view; // <-- here accessing data from `view` state.
const yy = doSomeLogicWith(y);
const z = doSomeMoreLogicWith(yy);
dispatch({ type: 'data/xAdded', payload: { x, z } });
};
Is this actually considered to be an anti-pattern in Redux? If so, what are the cons of doing this?
Yes, a thunk would qualify as "the code that dispatches the action" for this case. So, what the rule is recommending here is that if possible, the action would just contain y, and the function calls to doSomeLogicWith(y) and doSomeMoreLogicWith(yy) would ideally exist within the reducer instead.
Having said that, it's totally fine for a thunk to extract pieces of data from the state and include that in the action, and it's not wrong for a thunk to do some pre-processing of data before dispatching the action.
The style guide rule is just saying that, given a choice between running a particular piece of logic in a reducer or outside a reducer, prefer to do it in the reducer if at all possible.
I have now for umpteen months used this pattern for template level subscriptions in my Meteor/Blaze applications.
However, after upgrading to Meteor 1.4.3.2, I seem to have an odd bug on one of my templates.
I have this publication:
Meteor.publish('reports.byId', function(reportId){
console.log("subscribe reports.byId", reportId);
const reports = Reports.find({_id: reportId});
console.log(reports.count());
return reports;
});
I've removed any validation of user rights and have added the writes to check that I actually get data etc.
Now I'm calling this using this onCreated method:
Template.manageReport.onCreated(function(){
const instance = this;
const reportId = FlowRouter.getParam("reportId");
instance.autorun(function(){
const reportSub = instance.subscribe('reports.byId', reportId);
if (reportSub.ready()){
console.log("ready");
}
});
});
The odd thing is this: If I remove the check if the subscription is ready, everything works as expected. As soon as I check for the subscription readiness, the subscription is never ready and I can see via the log messages on the server that the subscriptions are requested to the tune of a few dozen times per second.
Yes, reportSub.ready() is reactive so it does trigger the autorun. Actually it's the only thing triggering your autorun. When subscription is ready it triggers autorun and then it's not ready anymore, because you've just called it again. It's a loop.
I think you should not call instance.subscribe and check for its readiness all inside the same autorun.
Since reportId is a route parameter and it's not going to change, you don't need autorun for the subscription. You would only need it if any parameter were a reactive variable that could change.
This should work just fine. Let me know how it goes.
Template.manageReport.onCreated(function() {
const instance = this;
const reportId = FlowRouter.getParam("reportId");
// Create subscription
const reportSub = instance.subscribe('reports.byId', reportId);
// Check when subscription is ready
instance.autorun(function() {
if (reportSub.ready()) {
console.log("ready");
}
});
});
I'm building an app where actions are performed as the user scrolls down. It would be nice if I could undo those actions as the user scrolls up again, basically turning scrolling into a way to browse through the time line of actions.
Is there a built-in way in Redux to do this? Or would I have to write middleware for this?
Is there a built-in way in Redux to do this? Or would I have to write middleware for this?
Middleware sounds like the wrong idea in this case because this is purely state management concern. Instead you can write a function that takes a reducer and returns a reducer, “enhancing” it with action history tracking along the way.
I outlined this approach in this answer, and it's similar to how redux-undo works, except that instead of storing the state, you can store actions. (Depends on the tradeoffs you want to make, and whether it's important to be able to “cancel” actions in a different order than they happened.)
I believe the idea is not so much "undo" as much as save a reference to the entire state tree each time an action passes through redux.
You would have a history stack made up of the application state at various times.
let history = [state1, state2, state3]
// some action happens
let history = [state1, state2, state3, state4]
// some action happens
let history = [state1, state2, state3, state4, state5]
// undo an action
let history = [state1, state2, state3, state4]
state = state4
To "undo" an action, you just replace the application state with one of the saved states.
This can be made efficient with data structures that support structural sharing, but in development we don't really need to consider resource constraints too much anyway.
I also wanted to create a simple undo functionality, but had already shipped an app with redux-storage that serializes and loads the state for every user. So to keep it backwards-compatible, I couldn't use any solution that wraps my state keys, like redux-undo does with past: [] and present:.
Looking for an alternative, Dan's tutorial inspired me to override combineReducers. Now I have one part of the state: history that saves up to 10 copies of the rest of the state and pops them on the UNDO action. Here's the code, this might work for your case too:
function shouldSaveUndo(action){
const blacklist = ['##INIT', 'REDUX_STORAGE_SAVE', 'REDUX_STORAGE_LOAD', 'UNDO'];
return !blacklist.includes(action.type);
}
function combineReducers(reducers){
return (state = {}, action) => {
if (action.type == "UNDO" && state.history.length > 0){
// Load previous state and pop the history
return {
...Object.keys(reducers).reduce((stateKeys, key) => {
stateKeys[key] = state.history[0][key];
return stateKeys;
}, {}),
history: state.history.slice(1)
}
} else {
// Save a new undo unless the action is blacklisted
const newHistory = shouldSaveUndo(action) ?
[{
...Object.keys(reducers).reduce((stateKeys, key) => {
stateKeys[key] = state[key];
return stateKeys;
}, {})
}] : undefined;
return {
// Calculate the next state
...Object.keys(reducers).reduce((stateKeys, key) => {
stateKeys[key] = reducers[key](state[key], action);
return stateKeys;
}, {}),
history: [
...(newHistory || []),
...(state.history || [])
].slice(0, 10)
};
}
};
}
export default combineReducers({
reducerOne,
reducerTwo,
reducerThree
});
For me, this works like a charm, it just doesn't look very pretty. I'd be happy for any feedback if this is a good / bad idea and why ;-)
There's no built-in way to do this.
but you can get inspired by how redux-dev-tools works (https://github.com/gaearon/redux-devtools). It basically have "time travel" functionality and it work by keep a track of all actions and reevaluating them each time. So you can navigate easily thorough all your changes.