I have an events database maintained in Firestore. I use AngularFire5 to keep the events synced to my app.
The events have many 'sessions', that can be updated my multiple administrators and users(ratings, questions etc).
I'm using ngrx store, reducers and effects to maintain the sync. I was able to figure out the single session update/delete/add.
The issue is that the initial load of sessions returns a session[]. And beyond that, the snapshot changes could return 'added', 'modified' and 'removed' session. How can I use a single action+reducer function to add when session(s) is not existing, but update or remove when session(s) existing. (I'm not calling store.dispatch within my firebase service, I'm using the effects to manage this)
session.effect.ts
#Effect()
getSession: Observable<Action> = this.actions.ofType(SessionActions.ADD_SESSION)
.map((action: SessionActions.AddSession) => action.payload)
.switchMap(payload => this.db.getSessions(payload)
.map((sessions:Session[]) => new SessionActions.AddSessionSuccess(sessions))
);
session.reducer.ts
export function SessionReducer(state: Session[], action: Action) {
// console.log('SessionsReducer :: Action Type: ' + action.type + ', Action Payload: ' + JSON.stringify(action.payload));
switch(action.type) {
case SessionActions.ADD_SESSION_SUCCESS:
return [ ...state, action.payload ];
case SessionActions.CREATE_SESSION_SUCCESS:
return [ ...state, action.payload ];
case SessionActions.UPDATE_SESSION_SUCCESS:
return state.map(session => {
return session.id === action.payload.id ? Object.assign({}, action.payload) : session;
})
case SessionActions.ADD_SESSION:
case SessionActions.DELETE_SESSION_FAILURE:
return state;
default:
return state;
}
}
session.actions.ts
//Used to search the sessions by event id, think of it as GetSessions
export class AddSession implements Action {
readonly type = ADD_SESSION;
constructor(public payload: string) {}
}
export class AddSessionSuccess implements Action {
readonly type = ADD_SESSION_SUCCESS;
constructor(public payload: Session[]) {}
}
Related
being rather new to react.js + redux, I'm facing the following conundrum:
I have multiple files, which need to update the store in exactly the same way, based on the stores current state. Currently I simply copy-paste the same code (along with the needed mapStateToProps), which goes again DRY.
Similar to something like the below, where getData is an Ajax call living in the actions file and props.timeAttribute is coming from mapStateToProps:
props.getData(props.timeAttribute).then((newState) => {
console.log(newState)
})
Would a function like that go in the actions file? Can the current state be read from within that actions file? Or does one normally create some sort of helperFile.js in which a function like that lives and is being called from other files?
Thanks!
If your file is executing the same action, then yes, you would put the action creator in a separate file and export it. In theory, you can put state in an action by passing the state as a parameter, but the philosophy behind an action is that it announces to your application that SOMETHING HAPPENED (as denoted by the type property on the return value of the action function). The reducer function responsible for handling that type subsequently updates the state.
You can access the current state of the store inside of an action creator like this:
export const testAction = (someParam) => {
return (dispatch, getState) => {
const {
someState,
} = getState(); //getState gets the entire state of your application
//do something with someState and then run the dispatch function like this:
dispatch(() => {type: ACTION_TYPE, payload: updatedState})
}
I like this approach because it encapsulates all the logic for accessing state inside of the one function that will need to access it.
DO NOT modify the state inside of the action creator though! This should be read only. The state of your application should only be updated through your reducer functions.
Yes, it is recommended to maintain a separate file for your actions.
Below is an example of how i use an action to fetch information and dispatch an action.
export const fetchComments = () => (dispatch) => {
console.log("Fetch Comment invoked");
/*you can use your Ajax getData call instead of fetch.
Can also add parameters if you need */
return fetch(baseUrl + 'comments')
.then(response => {
if (response.ok){
return response;
}
else {
var error = new Error('Error ' + response.status + ': ' + response.statusText);
error.response = response;
throw error;
}
},
error => {
var errmess = new Error(error.message);
throw errmess;
})
.then(response => response.json())
.then(comments => dispatch(addComments(comments)))
.catch(error => dispatch(commentsFailed(error.message)));
}
/* Maintain a separate file called ActionTypes.js where you can store all the ActionTypes as Strings. */
export const addComments = (comments) => ({
type : ActionTypes.ADD_COMMENTS,
payload : comments
});
export const comments = (errMess) => ({
type : ActionTypes.COMMENTS_FAILED,
payload : errMess
});
Once, you receive dispatch an action, you need an reducer to capture the action and make changes to your store.
Note that this reducer must be a pure function.
export const comments = (state = { errMess: null, comments:[]}, action) => {
console.log("inside comments");
switch (action.type) {
case ActionTypes.ADD_COMMENTS:
return {...state, errMess: null, comments: action.payload};
case ActionTypes.COMMENTS_FAILED:
return {...state, errMess: action.payload};
default:
return state;
}
};
Don't forget to combine the reducers in the configureStore().
const store = createStore(
combineReducers({
comments
}),
applyMiddleware(thunk,logger)
);
In your components where you use the Actions, use
const mapDispatchToProps = dispatch => ({
fetchComments : () => dispatch(fetchComments()),
})
Note to export the component as
export default connect(mapStateToProps,mapDispatchToProps)(Component);
I'm building an application using React/Redux, I have an array of products which are loaded to Redux state asynchronously and from which I would then like to pull individual products. However, the reducer I have written to do this isn't working as it is registering state as null. This is confusing me as calling getState() in the thunk action creator prior to returning the action and triggering the reducer is logging the correct state with the array of products.
Is this an error in my code or simply part of how redux state updates?
ACTION CREATOR: getSingleProduct
export const getSingleProduct = productName => (dispatch, getState) => {
const action = { type: 'GET_SINGLE_PRODUCT', productName };
if (!getState().products.length) return dispatch(getAllProducts())
.then(() => {
console.log('STATE IN ACTION CREATOR THEN BLOCK', getState());
return dispatch(action);
})
.catch(console.log);
else return action;
}
REDUCER: currentProduct
const currentProduct = (state = null, action) => {
switch (action.type) {
case 'GET_SINGLE_PRODUCT':
console.log('STATE IN REDUCER', state);
return state.products.filter(prod => prod.name.toLowerCase() === action.productName)[0];
break;
default:
return state;
}
}
Console Log Output
STATE IN ACTION CREATOR THEN BLOCK
{ basket: Array(0), products: Array(6), currentProduct: null }
STATE IN REDUCER
null
State is null cause you defined it as null on first function call.
console.log state after action complete and you see value run.
It is wrong to return modified state. Should return new state.
const currentProduct = (state = null, action) => {
switch (action.type) {
case 'GET_SINGLE_PRODUCT':
console.log('STATE IN REDUCER', state);
const products = state.products.slice().filter(prod => prod.name.toLowerCase() === action.productName)[0];
return { ...state, products }
break;
default:
return state;
}
}
Reducer state was in fact up to date, the issue was a misunderstanding of how reducer state works. I was trying to utilise a dependent state which is not available from the state argument of the reducer. The resolution for me was to pass this information from a dependent state in on the action object.
action
export const getSingleProduct = (productName, products = []) => (dispatch, getState) => {
let action = {
type: 'GET_SINGLE_PRODUCT',
productName,
products: getState().products
}
if (!action.products.length) dispatch(getAllProducts())
.then(() => {
action = Object.assign({}, action, { products: getState().products });
dispatch(action);
})
.catch(console.log);
else return action;
}
reducer
const currentProduct = (state = {}, action) => {
switch (action.type) {
case 'GET_SINGLE_PRODUCT':
const currentProduct = action.products.filter(prod => prod.name.toLowerCase() === action.productName)[0];
return Object.assign({}, state, currentProduct);
default:
return state;
}
}
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);
})
);
I'm dispatching an ADD_SOURCE action from my component that when it success dispatches another ADD_SOURCE_SUCCESS:
this.store$
.select(fromRoot.getUserState)
.filter(user => user.id != null && user.logged)
.takeUntil(this.componentDestroyed$)
.do(user => this.store$.dispatch({type: 'ADD_SOURCE', payload: user.username}))
.subscribe();
This is the effect that returns the ADD_SOURCE_SUCCESS according to a net call:
#Effect({ dispatch: true })
addSource$: Observable<Action> = this.actions$
.ofType('ADD_SOURCE')
.switchMap(
(action: Action) =>
this.userService.addCard(action.payload.username, action.payload.token)
.map((card: CardDTO) => {
return <Action>{
type: 'ADD_SOURCE_SUCCESS',
payload: <ICard>{ ... }
};
})
.catch(_ => {
return Observable.of(<Action>{ type: 'ADD_SOURCE_FAILED', payload: { }});
}));
So, then a new ADD_SOURCE_SUCCESS is dispatched on my reducer:
private static saveSourceSuccess(sourcesRdx, type, payload) {
return <ISourceRedux>{
ids: [ ...sourcesRdx.ids, payload.id ],
entities: Object.assign({}, sourcesRdx.entities, {[payload.id]: payload}),
selectedIds: sourcesRdx.selectedIds,
editingSource: null
};
}
Nevertheless, I don't quite figure out how to say on my component that the operation has been success and do one thing or another one...
Any ideas?
in your reducer, you should also handle the success & fail errors, set the store with an error message or whatever you need as data, and make a selector on it. Then the ui should subscribe to this new selector and you ll be notified
check this chapter
selectors
I have an action, that uses a redux thunk, that looks like so:
export function fetchData(query) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(someOtherAction(json)) })
}
}
}
and then my someOtherAction actually updates state:
export function someOtherAction(data) {
return {
action: types.SOME_ACTION,
data
}
}
But i want it to be possible for the fetchData action creator to be reusable so that different parts of my app can fetch data from myapi and then have different parts of the state based on that.
I'm wondering what is the best way to reuse this action? Is it acceptable to pass a second parameter in to my fetchData action creator that stipulates which action is called on a successful fetch:
export function fetchData(query, nextAction) {
return dispatch => {
return fetch(`http://myapi?query=${query}` ,{mode: 'cors'})
.then(response => response.json())
.then(json => { dispatch(nextAction(json)) })
}
}
}
Or is there an accepted way of doing this sort of thing?
I use a middleware for that. I have defined the fetch call in there, then in my actions I send the URL to fetch and the actions to dispatch when completed. This would be a typical fetch action:
const POSTS_LOAD = 'myapp/POST_L';
const POST_SUCCESS = 'myapp/POST_S';
const POST_FAIL = 'myapp/POST_F';
export function fetchLatestPosts(page) {
return {
actions: [POSTS_LOAD, POST_SUCCESS, POST_FAIL],
promise: {
url: '/some/path/to/posts',
params: { ... },
headers: { ... },
},
};
}
When calling that action, the POST_LOAD action will be dispatch automatically by the middleware just before the fetch request it's executed. If everything goes well the POST_SUCCESS action will be dispatched with the json response, if something goes wrong the POST_FAIL action will be dispatched by the middleware.
All the magic it's in the middleware! And it's something similar to this:
export default function fetchMiddleware() {
return ({ dispatch, getState }) => {
return next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
const { promise, actions, ...rest } = action;
if (!promise) {
return next(action);
}
const [REQUEST, SUCCESS, FAILURE] = actions;
next({ ...rest, type: REQUEST }); // <-- dispatch the LOAD action
const actionPromise = fetch(promise.url, promise); // <-- Make sure to add the domain
actionPromise
.then(response => response.json())
.then(json => next({ ...rest, json, type: SUCCESS })) // <-- Dispatch the success action
.catch(error => next({ ...rest, error, type: FAILURE })); // <-- Dispatch the failure action
return actionPromise;
};
};
}
This way I have all my requests on a single place and I can define the actions to run after the request it's completed.
------------EDIT----------------
In order to get the data on the reducer, you need to use the action name you defined on the original action creator. The following example shows how to handle the POST_SUCCESS action from the middleware to get the posts data from the json response.
export function reducer(state = {}, action) {
switch(action.type) {
case POST_SUCCESS: // <-- Action name
return {
...state,
posts: action.json.posts, // <-- Getting the data from the action
}
default:
return state;
}
}
I hope this helps!