Reputation: 13
I am implementing auto-save for a react-redux-firebase side project using redux-observable. Currently I have a updateFretboardEpic that is responding to any actions that modify a Fretboard component currently possessing a reference (firebaseKey) to the Firebase database. After 1 second debounce, updateFretboard should save the new state of the component to Firebase.
import {
ADD_STRING,
REMOVE_STRING,
INC_STR_PITCH,
DEC_STR_PITCH,
ADD_FRET,
REMOVE_FRET,
UPDATE_FRETBOARD
} from '../actions/action-types';
import {
updateFretboard
} from '../actions/fretboard-actions';
export const updateFretboardEpic = (action$, store) => {
return action$.ofType(
ADD_STRING,
REMOVE_STRING,
INC_STR_PITCH,
DEC_STR_PITCH,
ADD_FRET,
REMOVE_FRET
)
.filter(() => store.getState().fretboard.firebaseKey !== undefined)
.debounceTime(1000)
.map(() => updateFretboard(
store.getState().fretboard.fretboardData,
store.getState().fretboard.firebaseKey
));
};
However, I am currently getting the following error:
Uncaught Error: Actions must be plain objects. Use custom middleware for async actions.
at Object.performAction (<anonymous>:3:2312)
at liftAction (<anonymous>:2:27846)
at dispatch (<anonymous>:2:31884)
at bundle.ff2509b….js:9896
at SafeSubscriber.dispatch [as _next] (vendor.ff2509b….js:51341)
at SafeSubscriber.__tryOrUnsub (bundle.ff2509b….js:392)
at SafeSubscriber.next (bundle.ff2509b….js:339)
at Subscriber._next (bundle.ff2509b….js:279)
at Subscriber.next (bundle.ff2509b….js:243)
at SwitchMapSubscriber.notifyNext (bundle.ff2509b….js:6579)
Prior to implementing redux-observable, updateFretboard was using Redux-Thunk to dispatch an action:
export function updateFretboard(fretboardData, firebaseKey = undefined) {
return dispatch => { fretboards.child(firebaseKey).update(fretboardData); };
}
Using that as it stands with redux-observable will produce the error without any auto-save. Instead of returning a thunk, I changed it to this:
export function updateFretboard(fretboardData, firebaseKey = undefined) {
return fretboards.child(firebaseKey).update(fretboardData);
}
Interestingly, updateFretboardEpic will auto-save for the first action in the stream, return the error, and will not auto-save for any subsequent actions thereafter. updateFretboard does not currently flow through any of my reducers (it is only responsible for passing new state to Firebase), although I may choose in the future to receive a promise to know when the save occurred and pass it through my reducers.
I am new to RxJS/redux-observable, so I suspect there is a better way of doing this. Thoughts?
Upvotes: 0
Views: 1271
Reputation: 15401
When using redux-observable without any other side effect middleware (like redux-thunk) means all actions you dispatch must be plain old JavaScript objects--that includes anything your epics emits.
It's not clear what updateFretboard()
returns, except that is probably isn't a POJO action; it's whatever fretboards.child(firebaseKey).update(fretboardData)
returns.
If instead of emitting an action, you actually meant to just perform that as a side effect but ignore its return value entirely, you would use something like the do()
operator, which is used to make a side effect without actually modifying the next'd values. You could then combine that with the ignoreElements()
operator to prevent your epic from emitting anything ever.
export const updateFretboardEpic = (action$, store) => {
return action$.ofType(
ADD_STRING,
REMOVE_STRING,
INC_STR_PITCH,
DEC_STR_PITCH,
ADD_FRET,
REMOVE_FRET
)
.filter(() => store.getState().fretboard.firebaseKey !== undefined)
.debounceTime(1000)
.do(() => updateFretboard(
store.getState().fretboard.fretboardData,
store.getState().fretboard.firebaseKey
))
.ignoreElements();
};
Keep in mind that by using ignoreElements()
this particular epic will never emit anything (though it will still propagate errors/complete). It becomes basically "readonly".
If you didn't use ignoreElements()
, your epic would actually re-emit the same action it matched, causing unwanted recursion.
You might also find it easier to invert control of what gets saved and what doesn't. Instead of having to maintain a list of actions that should trigger an autosave, you could instead have actions have some sort of property that your epic listens for to know to save.
e.g.
// Listens for actions with `autosave: true`
export const updateFretboardEpic = (action$, store) => {
return action$.filter(action => action.autosave)
// etc...
};
// e.g.
store.dispatch({
type: ADD_STRING,
autosave: true
});
Adjust convention to your apps needs, accordingly.
Upvotes: 3
Reputation: 13
I found a solution to my problem (thanks Jay for pointing me in the right direction!).
In my question, I wasn't explicit about what fretboards.child(firebaseKey).update(fretboardData) actually is. fretboards.child(firebaseKey) is a reference to the particular child of fretboards with the specified firebase key (the full path would be firebase.database().ref('fretboards').child(firebaseKey)). Firebase database utilizes a Promise-based API, so update will return a Promise:
const updateRequest=fretboards.child(firebaseKey).update(fretboardData)
.then(function(result) {
// do something with result
}, function(err) {
// handle error
});
Borrowing from another answer Jay gave on a different post (Use fetch instead of ajax with redux-observable), I came up with the following solution:
import { fretboards } from '../firebase-config.js';
import Rx from 'rxjs/Rx';
import {
ADD_STRING,
REMOVE_STRING,
INC_STR_PITCH,
DEC_STR_PITCH,
ADD_FRET,
REMOVE_FRET,
AUTOSAVE_FRETBOARD
} from '../actions/action-types';
function autoSaveFretboard(fretboardData, firebaseKey = undefined) {
let autoSaveFretboardRequest = fretboards.child(firebaseKey)
.update(fretboardData).then(function(result) {
// won't need to pass result through reducers, so return
// true to indicate save was successful
return true;
}, function(err) {
// log error and return false in case of autosave failure
console.log(err);
return false;
});
return Rx.Observable.from(autoSaveFretboardRequest);
}
export const autoSaveFretboardEpic = (action$, store) => {
return action$.ofType(
ADD_STRING,
REMOVE_STRING,
INC_STR_PITCH,
DEC_STR_PITCH,
ADD_FRET,
REMOVE_FRET
)
.filter(() => store.getState().fretboard.firebaseKey !== undefined)
.debounceTime(750)
.mergeMap(() => autoSaveFretboard(
store.getState().fretboard.fretboardData,
store.getState().fretboard.firebaseKey
).map((res) => (
{ type: AUTOSAVE_FRETBOARD, success: res }
))
);
};
Based on whether the auto-save promise is successful or not, a boolean will be returned to indicate the success of the save, which is mapped to a new action AUTOSAVE_FRETBOARD which I can pass along to my reducers.
As Jay pointed out in that prior post, promises can't be cancelled. Since the Firebase database is implemented as a Promise-based API, I don't really see a way around that. At the moment, I can't think of a reason why I would want to cancel the auto-save, so I'm happy with this solution!
Upvotes: 1