Реализация автосохранения в приложении React-Redux-Firebase с использованием Redux-Observable
Я внедряю автосохранение для стороннего проекта реагировать с избыточностью, используя наблюдаемую избыточность. В настоящее время у меня есть updateFretboardEpic, который отвечает на любые действия, которые изменяют компонент Fretboard, в настоящее время имеющий ссылку (firebaseKey) на базу данных Firebase. После 1 секунды отката updateFretboard должен сохранить новое состояние компонента в 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
));
};
Однако в настоящее время я получаю следующую ошибку:
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)
До реализации наблюдаемого при редуксе updateFretboard использовал Redux-Thunk для отправки действия:
export function updateFretboard(fretboardData, firebaseKey = undefined) {
return dispatch => { fretboards.child(firebaseKey).update(fretboardData); };
}
Использование этого в том виде, в каком оно существует с redux-observable, приведет к ошибке без автоматического сохранения. Вместо того, чтобы вернуть thunk, я изменил его на это:
export function updateFretboard(fretboardData, firebaseKey = undefined) {
return fretboards.child(firebaseKey).update(fretboardData);
}
Интересно, что updateFretboardEpic автоматически сохранит для первого действия в потоке, вернет ошибку и не сохранит автоматически для последующих действий. updateFretboard в настоящее время не проходит ни через один из моих редукторов (он отвечает только за передачу нового состояния в Firebase), хотя в будущем я могу выбрать получение обещания знать, когда произошло сохранение, и передать его через мои редукторы.
Я новичок в RxJS/redux-observable, поэтому я подозреваю, что есть лучший способ сделать это. Мысли?
2 ответа
Когда используется наблюдаемый при редуксе без каких-либо других побочных эффектов, промежуточное ПО (например, при редуксе-thunk) означает, что все отправляемые вами действия должны быть простыми старыми объектами JavaScript - включая все, что испускает ваша эпопея.
Не понятно что updateFretboard()
возвращает, за исключением того, что это, вероятно, не действие POJO; это что угодно fretboards.child(firebaseKey).update(fretboardData)
возвращается.
Если бы вместо создания действия вы фактически хотели просто выполнить это как побочный эффект, но полностью игнорировать его возвращаемое значение, вы бы использовали что-то вроде do()
оператор, который используется для создания побочного эффекта без фактического изменения значений next'd. Затем вы можете объединить это с ignoreElements()
оператор, чтобы ваш эпос никогда не излучал ничего.
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();
};
Имейте в виду, что с помощью ignoreElements()
этот конкретный эпос никогда не будет излучать ничего (хотя он все равно будет распространять ошибки / завершено). Это становится в основном "только для чтения".
Если вы не использовали ignoreElements()
ваша эпопея на самом деле переиздаст то же действие, которое соответствует, вызывая нежелательную рекурсию.
Вам также может быть проще изменить контроль над тем, что спасено, а что нет. Вместо того, чтобы вести список действий, которые должны вызывать автосохранение, вы можете вместо этого иметь действия, обладающие каким-то свойством, которое ваш эпос прислушивается, чтобы сохранить.
например
// 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
});
Приспособьте соглашение к потребностям своих приложений, соответственно.
Я нашел решение своей проблемы (спасибо Джей за то, что указал мне правильное направление!).
В моем вопросе я не уточнил, что на самом деле представляет собой fretboards.child(firebaseKey).update(fretboardData). fretboards.child(firebaseKey) - это ссылка на конкретного потомка грифа с указанным ключом firebase (полный путь будет firebase.database().ref('fretboards').child(firebaseKey)). База данных Firebase использует API на основе Promise, поэтому обновление вернет Promise:
const updateRequest=fretboards.child(firebaseKey).update(fretboardData)
.then(function(result) {
// do something with result
}, function(err) {
// handle error
});
Заимствуя другой ответ, который Джей дал на другой пост ( используйте fetch вместо ajax с redux-observable), я нашел следующее решение:
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 }
))
);
};
В зависимости от того, выполнено ли обещание автосохранения или нет, будет возвращено логическое значение, указывающее на успех сохранения, которое сопоставлено с новым действием AUTOSAVE_FRETBOARD, которое я могу передать своим редукторам.
Как отметил Джей в этом предыдущем посте, обещания не могут быть отменены. Поскольку база данных Firebase реализована в виде API на основе Promise, я не вижу возможности обойти это. На данный момент я не могу придумать причину, по которой я хотел бы отменить автосохранение, поэтому я доволен этим решением!