I have two async requests I am trying to fulfill, the second based upon the results of the first. The way I am trying to do this is by:
Listen for success of first action: actions.GetAllItems
Select out from the store the relevant items based on ID: this.store.select(selectors.getItemsById)
Map over the returned IDs so I can make the second call for each item in the array of IDs returned by the first call
Put results in redux store, render to view.
The way I have now does successfully put it in my redux store. However since it's just vanilla Array.map it doesn't return an observable. Which means the observable isn't stored in this.details$, which means it does not render in my template with {{ details$ | async | json }}
How can I achieve this secondary XHR call based upon the results of the first?
ngOnInit() {
this.store.dispatch(new actions.GetAllItems())
this.details$ = this.actions$.pipe(
ofType(actions.types.GetAllItemsSuccess),
mergeMap(() => {
return this.store.select(selectors.getItemsById); // filter to multiple items based on item ID
}),
map((items: models.IItemGeneralResponse[]) => {
items.map(item => { // sync map does not seem like it belongs in rxjs
this.store.dispatch(
new actions.GetItemDetail(item.id)
);
});
})
);
}
You are trying to do ngrx effects stuff in your angular component. Use effects to handle side effects (calls to the backend/fetching data from local storage etc...) and make your component to watch for a piece of your state via a selector. Let's summarize like this -
Your component [or your guard or resolver] will just dispatch an action to the store.
If you set up a reducer for that action then your reducer will be called first otherwise it will go to step 3
In your effect, you are watching for the dispatched action. Your effect will make the first call and then from the response of the first call, it will make the second call and then it will update the state in your store [or piece of the state] which is being watched by your component by dispatching the respective actions to the store.
This is a typical workflow [It may vary as per the need of the app but the basic idea remains the same]. So keeping the basic idea lets modify your code like this -
In your component
sliceOfState$: Observable<any>; //change the type of observabe as per your app
ngOnInit() {
this.store.dispatch(new actions.GetAllItems())
//this observable will be used to render your data on UI
//you can use various rxjs operators to transform your data before shoing to UI
this.sliceOfState$ = this.store.select(//your selector which gives you sliceOfState);
}
Now In your effect -
#Effect()
this.details$ = this.actions$.pipe(
ofType(actions.types.GetAllItems),
switchMap(() => {
//here you call API which makes the call to backend which return allItems
return this.yourServiceWhichGetAllItems.getAllItems();
}),
switchMap(allItems => {
//now for each item you need to get its detail
//so forkJoin all the observables which calls the backedn for each item
const obs$ = allItems.map(item => this.yourServiceWhichGetDetails.getItemDetail(item));
return forkJoin(obs$);
})
map(allItemsWithDetails => {
//here you should call your action which will update the state in your store
return new actions.SetAllItemsDetails(allItemsWithDetails);
})
);
I have provided pseudo code which will give you an idea of how to achieve what you want to do. For more info, you can visit the official site of ngrx - https://ngrx.io/guide/effects
Related
My goal is to implement the ability to delete a specific document in a firebase collection, as a button in a component. To do this, I'm using an Angular service to get all the document IDs in a collection, as shown below (because the IDs also have utility in other components):
getCurrUserDocID(){
this.getUsers().subscribe(arr =>{
this.listUsers = arr.map(item => {
return{
userDocID : item.payload.doc.id,
...item.payload.doc.data() as User
}
})
})
return{
userIDs : this.listUsers
};
Injecting this service into components does allow me to get the docIDs. However, it's finicky. When I called it in ngOnInit in a component that loads under a navbar tab, it doesn't work unless I click that tab a couple times:
ngOnInit(): void {
console.log(this.service.getCurrUserDocID().userIDs)
}
The first and second click return an empty array in the console, while the third finally returns the actual array of data. I suspect this has something to do with observables and asychronous-ity.
You are using a subscription to get the data from this.getUsers() observer.
On getting new data you set the value of this.listUsers.
Now all you need in your component is to access this.listUsers.
Just remember that every time there will be data coming from this.getUsers() observer the value of this.listUsers will be overwritten which will cause your component to rerender.
The fact you get the data only on the third invocation may be related to the time it takes for this.getUsers() to return date or to the way you use it.
You have to notice that the subscription to the observable will return multiple results.
Don't subscribe out of component becouse you will need to unsubscribe it. If you want to sort data use map() operator in pipe().
getCurrUserDocID(){
this.listUsers = this.getUsers().pipe(
map(item => {
return {
userDocID: item.payload.doc.id,
...item.payload.doc.data() as User
}
})
)
};
In component
constructor(public: service: Service) {} // Make it public so you can use it in html file
ngOnInit(): void {
this.service.getCurrUserDocID() // here just triger `getUsers()` function.
}
In html template with async pipe
<ul>
<li *ngFor="let item of service.listUsers | async">
{{item.id}}
</li>
</ul>
Async pipe in template are subscribing and unsubscribing for you so you no need to worry about data leaks.
I'm trying to build a component that retrieves a full list of users from Amazon AWS/Amplify, and displays said results in a table via a map function. All good so far.
However, for the 4th column, I need to call a second function to check if the user is part of any groups. I've tested the function as a button/onClick event - and it works (console.logging the output). But calling it directly when rendering the table data doesn't return anything.
Here is what I've included in my return statement (within the map function)
<td>={getUserGroups(user.email)}</td>
Which then calls this function:
const getUserGroups = async (user) => {
const userGroup = await cognitoIdentityServiceProvider.adminListGroupsForUser(
{
UserPoolId: '**Removed**',
Username: user,
},
(err, data) => {
if (!data.Groups.length) {
return 'No';
} else {
return 'Yes';
}
}
);
};
Can anyone advise? Many thanks in advance if so!
Because you should never do that! Check this React doc for better understanding of how and where you should make AJAX calls.
There are multiple ways, how you can solve your issue. For instance, add user groups (or whatever you need to get from the backend) as a state, and then call the backend and then update that state with a response and then React will re-render your component accordingly.
Example with hooks, but it's just to explain the idea:
const [groups, setGroups] = useState(null); // here you will keep what "await cognitoIdentityServiceProvider.adminListGroupsForUser()" returns
useEffect(() => {}, [
// here you will call the backend and when you have the response
// you set it as a state for this component
setGroups(/* data from response */);
]);
And your component (column, whatever) should use groups:
<td>{/* here you will do whatever you need to do with groups */}</td>
For class components you will use lifecycle methods to achieve this (it's all in the documentation - link above).
I have an Observable listening to the URL and I am switching it to a getRows() which returns an Observable pulling data back from the API with the URL parameters. I want to be able get a Subscription reference for every emit that getRows() does. This is to show a loading indicator on the UI.
The current code:
this.tableSource = this.urlParamService.getParameterGroup(this.parameterPrefix)
.distinctUntilChanged()
.map(p => this.getRows(p))
.switch()
.share();
And then explicitly when I have changed the parameters I have been calling:
this.tableLoad = this.tableSource.take(1).subscribe((r) => this.rows = this.parseRows(r));
But I want to enable the component to update when external entities manipulate the URL and so I should be subscribing instead of sharing tableSource, so how can I get a Subscription everytime I call getRows(), is it possible?
I managed to solve it this way:
this.urlParamService.getParameterGroup(this.parameterPrefix)
.distinctUntilChanged()
.do(p => {
this.tableLoad = this.getRows(p).subscribe((r) => this.rows = this.parseRows(r));
})
.subscribe();
So I stopped trying to use both Observable sequences as one (even though one depends on the other), and just subscribed to the second as a side effect of the first.
I am using React + Flux for my application. General flow is I will call action method from JSX component and that action method will invoke store which updates state and emits change event and the state will be updated in JSX component.
Now the problem is, I have separated actions and stores into different files based on their work(2 action files and 2 store files).
Can I write this kind of code in JSX component ? (action2.method2() call is waiting for result of actions1.method1() ). If not, how can I improve this ? method1() and method2() are two different actions methods, so we can't call actions2.method2() inside body of actions1.method1() and also method2 needs access to state updated by method1().
this.getFlux().actions.actions1.method1()
.then(() => {
this.getFlux().actions.actions2.method2(this.state.method1UpdatedState);
})
.catch((error) => { console.log("error : ", error)} );
I would recommend keeping actions as simple as possible, and move all business logic to your stores. So, if you need data from store1 in store2 just inject store1 in your store2 as a dependency and listen to the specific event, generated by action1 in your store2. You definitely, should avoid implementing this logic in your component.
For example, this how store2.js should look
import store1 from './store1';
store2.dispatchToken = AppDispathcer.register((payload) => {
var action = payload.action;
switch(action) {
case 'ACTION_1':
AppDispathcer.waitFor([store1.dispatchToken]);
let data = store1.getDataFromAction1();
// ... here you can update store2 data
// ... and call store2.emitChange();
break;
}
});
Based on the scaffolder mern.io I was going through the code to see what was going on. I stumbled upon a .need method which looks like something related to es6 classes. I can't seem to find any usable info anywhere, so I ask what is the .need method?
class PostContainer extends Component {
//do class setup stuff here
}
PostContainer.need = [() => { return Actions.fetchPosts(); }];
You can get the project up and running very easily with these commands.
npm install -g mern-cli
mern YourAppName
The mern documentation is pretty terse when it comes to explaining this.
fetchComponentData collects all the needs (need is an array of actions that are required to be dispatched before rendering the component) of components in the current route. It returns a promise when all the required actions are dispatched.
Reading through the code is a much clearer way of finding out what's going on here.
Overview
It's a way to specify some actions that should be dispatched before rendering the component.
This component maps the posts property from the Redux store to a prop called posts so that it can render the list of posts.
// PostContainer.jsx
function mapStateToProps(store) {
return {
posts: store.posts,
};
}
However, initially this property will be empty because the posts need to be fetched from an asynchronous API.
// reducer.js
// initial state of the store is an empty array
const initialState = { posts: [], selectedPost: null };
This component needs the posts to be available before it renders, so it dispatches the action returned from the call to Actions.fetchPosts().
// actions.js
export function fetchPosts() {
return (dispatch) => {
return fetch(`${baseURL}/api/getPosts`).
then((response) => response.json()).
then((response) => dispatch(addPosts(response.posts)));
};
}
When the action has finished dispatching, the store's data can be mapped to the connected component.
Caveat
This isn't a universal way to specify asynchronous dependencies for React components. It only works because mern has a utility method called fetchComponentData that it calls at the server side, in order to populate the Redux store before rendering.
// server.js
fetchComponentData(store.dispatch, renderProps.components, renderProps.params)
This method traverses the components from the second argument to extract the needs from each. Then it executes 'needs` and waits for all the promises to complete.
// fetchData.js
const promises = needs.map(need => dispatch(need(params)));
return Promise.all(promises);
When the promise returned by Promise.all(promise) completes, the Redux store will be populated and the components can safely render their data to be served to the client.
Syntax
You mentioned that you thought it might be related to ES6 classes, so I'll cover the syntax quickly too.
ES6 classes can't have static properties specified in the class literal, instead we have to declare them as properties on the class after it has been defined.
The needs property must be an array of functions that return promises to work with fetchComponentData. In this case we have an arrow function declared inside an array literal. It might help to look at it split up into separate variables.
const fetchPosts = () => { return Actions.fetchPosts() };
const needs = [fetchPosts];
PostContainer.need = needs;