I seek help from you for the first time as I am in deep trouble.
I am using ngrx effects to load some shop items into the cart on app init from firebase realtime database. As the payload I get the whole firebase database snapshot object instead of just the shop items themselves, therefore the ngrx store receives an object it cannot understand and the app state does not change.
Check the photos please: when console.logged(), I see the exact objects that I need. But the Redux Devtools reveal the real deal, and I do not know how to fix this. Could somebody give me advice? Thank you very, very much.
The effect looks like this:
#Effect()
getShopItems: Observable<any> = this.actions.pipe(
ofType(shopActions.GET_ITEMS),
switchMap(() => {
return of(this.database.ref('cart').once('value', snap => {
snap.forEach(childSnap => childSnap.val());
// snap.forEach(childSnap =>
// console.log(childSnap.val()));
}))
.pipe(
map((payload) => {
return {
type: 'GET_ITEMS_SUCCESS',
payload: payload
};
}
));
})
);
The reducer functions for the actions in question look like:
case shopActions.GET_ITEMS: {
return {
...state,
shopItems: [...state.shopItems],
itemCount: state.shopItems.length
};
}
case shopActions.GET_ITEMS_SUCCESS: {
return {
...state,
shopItems: [action.payload],
itemCount: state.shopItems.length + 1
};
}
https://imgur.com/a/pDGAwFI
Use from rather than of:
import { from } from 'rxjs';
#Effect()
getShopItems: Observable<any> = this.actions.pipe(
ofType(shopActions.GET_ITEMS),
switchMap(() => {
return from(this.database.ref('cart').once('value', snap => {
snap.forEach(childSnap => childSnap.val());
}))
.pipe(
map((payload) => {
return {
type: 'GET_ITEMS_SUCCESS',
payload: payload
};
}
));
})
);
RxJs from - https://www.learnrxjs.io/operators/creation/from.html
Related
I am trying to modify an effect I have made into letting me start and stop multiple firestore queries by using two actions. Currently the effect allows me to start and stop a single firestore query by listening for two separate actions in the effect. I simply use a switchMap to switch into an empty observable when there is a stop action. This works just fine.
#Effect()
startStopQuery$ = this.actions$.pipe(
ofType(
ActionTypes.START,
ActionTypes.STOP
),
switchMap(action => {
if (action.type === ActionTypes.STOP) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', 'x');
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
What I actually want to do is to have multiple queries ongoing that I can start and stop with those same two actions, but where it depends on the action payload. When I modified it everytime I performed a new query the last one stops working. I think it is because the switchMap operator switches away from my last query observable. This is the best I have come up with:
#Effect()
startStopQueryById$ = this.actions$.pipe(
ofType(
ActionTypes.START_BY_ID,
ActionTypes.STOP_BY_ID
),
switchMap(action => {
if (action.type === ActionTypes.STOP_BY_ID) {
return of([]);
} else {
return this.afs.collection('collection', ref => {
return ref.where('field', '==', action.id);
}).stateChanges();
}
}),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
As I said, I think the issue is the switchMap operator. But that is also what I depended on to make the "stop" work in the first place. I cant seem to come up with another solution as I am not very well versed in the style yet.
Any help would be greatly appreciated!
I came up with a solution. I make an object that maps ID's to the firestore statechanges observables. On the start action I make the listener and adds it to the object. I make sure that it automatically unsubscribe by piping takeUntil with the corresponding stop action. It returns a merge of all the observables in the object and I silply do as before. I also have a seperate effect triggered by the stop action to remove the observable from the object. It looks like so:
queriesById: {[id: string]: Observable<DocumentChangeAction<Element>[]>} = {};
#Effect()
startQuery$ = this.actions$.pipe(
ofType(ActionTypes.START_BY_ID),
switchMap(action => {
this.queriesByPlay[action.pid] = this.afs.collection<Element>('requests', ref => {
return ref.where('field', '==', action.id);
}).stateChanges().pipe(
takeUntil(
this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
filter(cancelAction => action.id === cancelAction.id),
)
)
);
return merge(
Object.values(this.queriesByPlay)
);
}),
mergeMap(actions => actions),
mergeMap(actions => actions),
map(action => {
return {
type: `[Collection] ${action.type}`,
payload: { id: action.payload.doc.id, ...action.payload.doc.data() }
};
})
);
Effect({dispatch: false})
stopQuery$ = this.actions$.pipe(
ofType(ActionTypes.STOP_BY_ID),
map(action => delete this.queriesByPlay[action.id]),
);
This seems to work and have no problems except for being convoluted hard to understand.
I'm trying to figure out the correct way to use firestore.onSnapshot with react-redux.
I currently have this code in my action file, which I am calling on componentWillMount() in my component.
export const fetchCheckins = () => async (dispatch) => {
const {currentUser} = firebaseService.auth();
try {
let timestamp = (new Date());
//set timestamp for beginning of today
timestamp.setHours(0);
//get checkins today
let checkinstoday = (await firebaseService.firestore().collection(`/checkins/${currentUser.uid}/log`).where("timestamp",">=",timestamp).orderBy("timestamp","desc").get()).docs.map(doc => doc.data());
//set timestamp for beggining of week
timestamp.setDate(-(timestamp.getDay()));
//get checkins (week)
let checkinsweek = (await firebaseService.firestore().collection(`/checkins/${currentUser.uid}/log`).where("timestamp",">=",timestamp).orderBy("timestamp","desc").get()).docs.map(doc => doc.data());
//set timestamp for begging of month
timestamp.setDate(0);
//get checkins (month)
let checkinsmonth = (await firebaseService.firestore().collection(`/checkins/${currentUser.uid}/log`).where("timestamp",">=",timestamp).orderBy("timestamp","desc").get()).docs.map(doc => doc.data());
dispatch({type: FETCH_CHECKINS, payload: { today: checkinstoday, week: checkinsweek, month: checkinsmonth}});
}
catch(e){
console.error(e);
}
};
this works fine, the correct data is sent to the component and display. The problem is, that if the user checks in, the checkin data should adjust, but it does not, since I am getting the data once and sending it, and the state is not re-rendering.
My question is how I should approach this? Do I use .onSnapshot() instead of .get()? Do I call .fetchCheckins() from the .checkin() action creator? How do I approach according to best practice? thank you
According to firestore's documentation if you need realtime updates you should use onSnapshot:
https://firebase.google.com/docs/firestore/query-data/listen
In your case if you use .get() - you get the update once and firestore won't notify you if any of the data changes. That's why you are not seeing the changes.
P.S. checkout redux-firestore: https://github.com/prescottprue/redux-firestore - it's nice library that can help you with your redux bindings.
You could subscribe your list like this:
function subscribeToExperiences() {
return eventChannel((emmiter: any) => {
experiencesRef.onSnapshot({ includeMetadataChanges: true }, snapshot => {
const experiences: IExperience[] = snapshot.docChanges().map(change => ({
id: change.doc.id,
title: change.doc.data().title
}));
if (snapshot.docChanges().length !== 0) {
emmiter(experiences);
}
});
return () => experiencesRef;
});
}
function* fetchExperiences(_: ExperiencesFetchRequested) {
const channel = yield call(subscribeToExperiences);
try {
while (true) {
const experiences = yield take(channel);
yield put(new ExperiencesFetchSucceeded(experiences));
}
} finally {
if (yield cancelled()) {
channel.close();
}
}
}
subscribeToExperiences uses a redux-saga eventChannel. An eventChannel receives an emmiter that generates a saga effect to be consumed with take. The eventChannel has to return a function to close the connections but afaik .onSnapshot connections don't need to be explicitly closed, that's why I return a dummy function.
it's possible to create an epic to make data flows from firestore through redux? I'm trying do listen to firestore changes an put that on my redux but I get this error on my epic:
Actions must be plain objects. Use custom middleware for async actions
From what I get until now this error is happenning because the epic is dispatching an observable instead of a object right?
how can I convert this observable that contains the list of changes inside it and dispatch all changes to redux?
const handlePosts = snapshot => {
snapshot.docChanges.forEach((change) => {
if (change.type === 'added') {
return {
type: ADD_POST,
info: change.doc.data()
}
}
if (change.type === 'modified') {
return {
type: UPDATE_POST,
info: change.doc.data()
}
}
if (change.type === 'removed') {
return {
type: REMOVE_POST,
info: change.doc.data()
}
}
});
};
const posts$ = Rx.Observable.create(observer => firestoreDb
.collection('posts')
.onSnapshot(observer)
);
export const firestoreStreamEpic = (action$, store) =>
action$.ofType(START_FIRESTORE_STREAM)
.switchMap(action =>
.map((payload) => {
handlePosts(payload);
})
);
Usually I use redux-saga, but currently I need redux-thunk. I'm using ducks for modular structure and now for example I have two ducks: auth and user with async actions below:
auth-duck.js
register(credentials) {
return dispatch => {
dispatch(actions.registerRequest());
return service.requestRegister(credentials)
.then((response) => {
dispatch(actions.registerSuccess(...));
// Here I need to dispatch some action from user-duck.js
})
.catch(() => dispatch(actions.registerError(...)))
}
}
user-duck.js
fetchUser() {
return dispatch => {...}
}
I really don't know how to not mess these two modules and dispatch fetchUser after successful register.
I could return register result (e.g. token or something else) to container from here it was dispatched and then using chaining dispatch fetchUser.
AuthContainer.js
_onSubmit() {
this.props.register().then(() => this.props.fetchUser);
}
But I don't know is it the best way to manage such operations with redux-thunk?
There is no rule thunks can only make one HTTP request. If you need to fetch the user after login, then fetch it.
const login = credentials => dispatch => {
fetchLogin(credentials).then(() => {
dispatch({ type: "LoginSuccess" })
return fetchUser()
}).then(() => {
dispatch({ type: "UserFetched" })
})
}
If you want to reuse the user fetch action, then dispatch a thunk from a thunk.
const fetchCurrentUser = login => dispatch => {
return fetchUser(login.userId).then(user => {
dispatch({ type: "UserLoad" })
return user
})
}
const login = credentials => dispatch => {
return fetchLogin(credentials).then(login => {
dispatch({ type: "LoginSuccess" })
return dispatch(fetchCurrentUser(login))
}
}
In one of my apps, I call 7 action thunks after successful login.
After a long search I found two options how to share the logic from separate domains.
The first one is to use mapDispatchToProps (Thanks #DonovanM), just like this:
function mapDispatchToProps(dispatch) {
return {
login: (credentials) => {
return dispatch(authActions.login(credentials)).then(
() => dispatch(userActions.fetchUser())
);
}
}
}
login function returns Promise thats why we can chain it to another one.
And the second possible option:
Use something like a "bridge" file in our case it is app-sagas.js
app-duck.js
import {actions as authActions} from './auth-duck.js';
import {actions as userActions} from './user-duck.js';
export function doLogin(credentials) {
return dispatch => {
return dispatch(authAction.login(credentials)).then(
() => dispatch(userActions.fetchUser())
);
}
}
In the second case we can place all logic into ducks and avoid spreading the logic within containers. But I guess it is possible to combine both methods.
Recently I've been looking into react and redux. I read up the official documentation and tried some ToDo List tutorials. Part 1 is just about react and this is part 2 about redux:
http://www.theodo.fr/blog/2016/03/getting-started-with-react-redux-and-immutable-a-test-driven-tutorial-part-2/
So basically he just sets up a store and initially adds an array of a few todos. Now I don't want my data to be local and I want to fetch it from an API. I'm having a hard time understanding how this actually works. So the code I would use in my action_creators.js is:
export function fetchData() {
return dispatch => {
fetch('http://127.0.0.1:8000/example')
.then(res => res.json())
.then(res => dispatch({
type: FETCH_DATA,
data: res
}))
}
}
Now in the example code for example adding a 'todo':
export function addItem(text) {
return {
type: 'ADD_ITEM',
text
}
}
You aren't dispatching anything, the tutorial does this in the reducer? But when you return dispatch your fetch, does this automatically get dispatched to your store?
If so I have no clue what I should write in my reducer ..
This is the code I have for adding a 'todo':
import {Map} from 'immutable';
function setState(state, newState) {
return state.merge(newState);
}
function fetchData(state) {
return state;
}
function addItem(state, text) {
const itemId = state.get('hostnames').reduce((maxId, item) => Math.max(maxId,item.get('id')), 0) + 1;
const newItem = Map({id: itemId, text: text, status: 'active'});
return state.update('hostnames', (hostnames) => hostnames.push(newItem));
}
export default function(state = Map(), action) {
switch (action.type) {
case 'SET_STATE':
return setState(state, action.state);
case 'ADD_ITEM':
return addItem(state, action.text);
case 'FETCH_DATA':
return fetchData(state);
}
return state;
}
So basically my question is, how do I fetch the data ( if the fetch is wrong now ) and how do I add the fetched data from my api to the store in my reducer.
I just find react and redux pretty complicated so sorry if I'm asking a really noob question or just making big mistakes in the way I want to do something.
Thanks in advance for any help.
imagine your json
{
"data": {
"apple": 1,
"banana": 3,
},
"status": 200,
}
your actions
export function fetchData() {
return dispatch => {
fetch('http://127.0.0.1:8000/example')
.then(res => res.json())
.then((responseData) => {
if(responseData.status === 200){
dispatch(setData(responseData));
}
})
}
}
export function setData(responseData) {
return {type: SET_DATA, data: responseData.data }
}
your reducer
const initialState = { data: null };
export default function(state = initialState, action) {
switch (action.type) {
case 'SET_DATA':
return Object.assign({}, state, {
data: action.data,
})
default:
return state;
}
}
then your state will become
{ data: {
apple: 1,
banana: 3,
}
}
Actually, all your reducers should be pretty dumb and pure (without any side effects). So their only concern is to modify the state and nothing else. Fetching data from the server or any kind of orchestration should be implemented in redux middleware. Look at redux-thunk or redux-saga if you need something more complicated. Hope that helps.