I am testing the React-table server side data to render a huge amount of data fetched from an web api without crashing the browser. With the base react-table settings the browser is unable to handle such amount of records (500000) and crash (it gets stuck in the pending state of the request).
So I find the server side data that maybe can help me.
I followed the instructions from the documentation but typescript is complaining about the data that I am trying to use when I update the state.
This is what I have until now:
The method that fetch the data from web api:
private fetchSales() {
fetch(`http://localhost:50335/api/RK`)
.then(response => response.json())
.then(data =>
this.setState({
sales: data // here I get 500000 items
})
)
}
This fetchSales gets called in the componentDidMount().
Then I have the ReactTable component inside the render():
render() {
const {
sales,
pages
} = this.state;
return (
<div className = "App" >
<ReactTable
data = {sales}
manual
pages = {pages}
defaultPageSize = {10}
onFetchData = {this._fetchData}
columns = {
[{
Header: "Region",
accessor: "Region"
},
{
Header: "Country",
accessor: "Country"
}]
}
/>
</div>
);
}
In the ReactTable there is call to a function called _fetchData and that function looks like this:
private _fetchData(state: any) {
requestData(
state.sales,
state.pageSize,
state.page
).
then(res => {
this.setState({
sales: res.rows, // here typescript complain: "res is of type 'unknown'"
pages: res.pages // here typescript complain: "res is of type 'unknown'"
});
})
}
Inside the setState the res object is type 'unknown' and typescript doesn't like it.
requestData is a function that lives outside the class and get the sales, pageSize and page states:
const requestData = (sales: any, page: number, pageSize: number) => {
return new Promise((resolve, reject) => {
const res = {
rows: sales.slice(pageSize * page, pageSize * page * pageSize),
pages: Math.ceil(sales.length / pageSize)
}
resolve(res);
})
}
The function is almost identical the in the documentation I only removed the filtering and sorting because I don't need them. I only need the res object that return the function.
And I almost forget it, inside the constructor I am attaching the this to the _fetchData method: this._fetchData = this._fetchData.bind(this);
Why is typescript complaining about the res object that I am trying to use to set the state?
Best regards!
Americo
EDIT
I noticed a few mistakes.
private _fetchData(state: any) {
requestData(
state.sales, // that should be state.data (state is the state of ReactTable)
state.pageSize,
state.page
...
Next, I've noticed that you do pagination after fetching the data. But using manual with onFetchData is for handling pagination on the server. For example, with page and pageSize you could pass these parameters to your API. That's the whole point of pagination!
An example from the documentation:
onFetchData={(state, instance) => { //onFetchData is called on load and also when you click on 'next', when you change page, etc.
// show the loading overlay
this.setState({loading: true})
// fetch your data
Axios.post('mysite.com/data', {
//These are all properties provided by ReactTable
page: state.page,
pageSize: state.pageSize,
sorted: state.sorted,
filtered: state.filtered
})
Since you're fetching all of them at once (why though? can't your API on the server handle pagination? this way, you won't have to wait ages before the results are returned), then I suggest you let ReactTable do the work.
That is, you just do:
<ReactTable
columns={columns}
data={data}
/>
ReactTable will take care of pagination. And now, you may use your query to the API in ComponentDidMount.
Could you please instead try to put all the logic in the onFetchData. I could be wrong but it seems to me you misunderstood the instructions in the documentation. OnFetchData is called at ComponentDidMount, it's not telling you that you have to put your function there.
private _fetchData(state, instance) {
const { page, pageSize } = state
fetch(`http://localhost:50335/api/RK`)
.then(response => response.json())
.then(data =>
let sales = data
this.setState({
sales: sales.slice(pageSize * page, pageSize * page * pageSize),
pages: Math.ceil(sales.length / pageSize)
});
)
}
As for Typescript, from what I gather, Typescript doesn't have enough information to infer the type of what your Promise returns.
So you have to explicitly annotate Promises generic type parameter:
return new Promise<{ sales: object; pages: number; }>((resolve, reject) => { ... }
Related
When using Apollo client, I find it quite tedious to manually update the cache for every mutation that requires an immediate UI update. I therefore decided to try to make a custom hook which updates the cache automatically.
The hook works but it seems a little "hacky" and I'm worried it might mess with the normal functioning of the cache. So I just wanted to ask if this hook seems like it should work ok?
Here's the code (where mutationName is the actual graphql mutation name and fieldName is the original graphql query name corresponding to the mutation):
export const useMutationWithCacheUpdate = (
mutation,
mutationName,
fieldName
) => {
const [createMutation, { data, loading, error }] = useMutation(mutation, {
update(cache, { data }) {
data = data[mutationName];
cache.modify({
fields: {
[fieldName]: (existingItems = []) => {
const newItemRef = cache.writeFragment({
data: data,
fragment: gql`
fragment newItem on ${fieldName} {
id
type
}
`,
});
return [...existingItems, newItemRef];
},
},
});
},
});
return [createMutation, { data, loading, error }];
};
In my next.js app i'm using the swr hook since it provides cache and real time updates, and that is quite great for my project ( facebook clone ), but, there's a problem.
The problem, is that, in my publications, i fetch them along with getStaticProps, and i just map the array and everything great, but, when i do an action, like, liking a post, or commenting a post, i mutate the cache, and i thought that what that does, is to ask the server if the information that is in the cache is right.
But, what it really does, is that, it makes another API call, and, the problem with that, is that, if i like a publication, after the call to the API to make sure everything is right in the cache, if there are 30 new publications, they will appear in the screen, and i don't want that, i want the pubs the user on screen requested at the beggining, imagine comenting on a post, then there are 50 new post so you lost the post where you commented...
Let me show you a litle bit of my code.
First, let me show you my posts interfaces
// Publication representation
export type theLikes = {
identifier: string;
};
export type theComment = {
_id?: string;
body: string;
name: string;
perfil?: string;
identifier: string;
createdAt: string;
likesComments?: theLikes[];
};
export interface Ipublication {
_id?: string;
body: string;
photo: string;
creator: {
name: string;
perfil?: string;
identifier: string;
};
likes?: theLikes[];
comments?: theComment[];
createdAt: string;
}
export type thePublication = {
data: Ipublication[];
};
This is where i'm making the call to get all posts
const PublicationsHome = ({ data: allPubs }) => {
// All pubs
const { data: Publications }: thePublication = useSWR(
`${process.env.URL}/api/publication`,
{
initialData: allPubs,
revalidateOnFocus: false
}
);
return (
<PublicationsHomeHero>
{/* Show pub */}
{Publications.map(publication => {
return <Pubs key={publication._id} publication={publication} />;
})}
</PublicationsHomeHero>
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const { data } = await axios.get(`${process.env.URL}/api/publication`);
return {
props: data
};
};
export default PublicationsHome;
And, for example, this is how i create a comment, i update cache, make the call to the API, then mutate to see if data is right
// Create comment
const handleSubmit = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
try {
mutate(
`${process.env.URL}/api/publication`,
(allPubs: Ipublication[]) => {
const currentPub = allPubs.find(f => f === publication);
const updatePub = allPubs.map(pub =>
pub._id === currentPub._id
? {
...currentPub,
comments: [
{
body: commentBody,
createdAt: new Date().toISOString(),
identifier: userAuth.user.id,
name: userAuth.user.name
},
...currentPub.comments
]
}
: pub
);
return updatePub;
},
false
);
await createComment(
{ identifier: userAuth.user.id, body: commentBody },
publication._id
);
mutate(`${process.env.URL}/api/publication`);
} catch (err) {
mutate(`${process.env.URL}/api/publication`);
}
};
Now, after creating the comment, as i already mentioned, it makes another call to the API, and if there are new posts or whatever, it will appear in the screen, and i just want to keep the posts i have or add new ones if i'm the one that created them.
So, let's say that i will like a post
Everything is great and fast, but, after making sure data is right, another post will appear because another user created it
Is there a way to make sure data is right without making another call to the API that will add more posts to the screen ?
I'm new to this swr hook, so, hope you can help me and thanks for your time !
Upate
There's a way to update cache without needing to refetch
many POST APIs will just return the updated data directly, so we don’t need to revalidate again. Here’s an example showing the “local mutate - request - update” usage:
mutate('/api/user', newUser, false) // use `false` to mutate without revalidation
mutate('/api/user', updateUser(newUser)) // `updateUser` is a Promise of the request,
// which returns the updated document
But, i don't kwow how should i change my code to implement this, any ideas !?
If you want to update the cache, and make sure everything is right, withouth having to make another call to the API, this seems like working
Change this
await createComment(
{ identifier: userAuth.user.id, body: commentBody },
publication._id
);
mutate(`${process.env.URL}/api/publication`);
For this
mutate(
`${process.env.URL}/api/publication`,
async (allPublications: Ipublication[]) => {
const { data } = await like(
{ identifier: userAuth.user.id },
publication._id
);
const updatePub = allPublications.map(pub =>
pub._id === data._id ? { ...data, likes: data.likes } : pub
);
return updatePub;
},
false
);
What you are doing there, is to update cache with the data that you'll receive data from the API's action, and you put it in the cache, but, you have to put false as well so it doesn't revalidate again, i tried it out and it's working, don't know if i'll have problems with it in the future, but for knowm it works great !
I have a function to get rates from products, so lets say I have one product with two rates. So my product has two rates. Then, when I get those rates I must get the prices attached to my product. So for each rate I have to look for its prices.
The next code below explains this:
this.loadProductInfo = true; // bool to load data in my form
// First of all, I get rates from API
// const rates = this._http....
// Now, for each rate I must search If my product/products have a price:
this.rates.forEach((rate, index, arr) => {
this._glbGetPricesForProduct.getPrice(params).subscribe(response => {
if (!arr[index + 1]) {
this.initForm();
this.loadProductInfo = false;
}
})
});
The variable loadProductInfo it loads content in my form, so in my html I have:
<form *ngIf="!loadProductInfo"></form>
But form it still give me error: could not find control name.
But if I do this instead, it works correctlly:
setTimeout(() => {
this.initForm();
this.loadProductInfo = false;
}, 2000);
So what I want its to say my form to wait until I have all code loaded and then after it, load its contents. But instead it cant find the control because it loads before code. Any help I really appreciate it.
The main mistake I see there is that you are looping over async data which may not be there when your code execute the for each loop (your rates).
I would build an observable with your rates as a source:
...
$rates: Observable<any> = this._http.get(...);
rates.pipe(
mergeMap((rates) => {
const priceByRates: Observable<any>[] = rates.map((rate, index, arr) => this._glbGetPricesForProduct.getPrice(params));
return combineLatest(pricesByRates); // if getPrice complete right away, use forkJoin() instead
})
).subscribe(res => {
// No need to check for the last item, all rates have been checked for possible price
this.initForm();
this.loadProductInfo = false;
});
...
This implementation should wait for your api calls to resolve before printing your form.
Since you are hiding the entire form, it may be better to just move the API call into a resolver so that the page does not render until the data is ready.
Here is a minimal StackBlitz showcasing this behavior: https://stackblitz.com/edit/angular-ivy-4beuww
Component
In your component, include an ActivatedRoute parameter via DI.
#Component(/*omitted for brevity*/)
export class MyComponent {
constructor(private route: ActivatedRoute) {
// note: 'data' is whatever you label your resolver prop in your routing setup
route.data.subscribe(resolved => {
if ("data" in resolved) this.resolveData = resolved["data"];
});
}
}
Route Setup
And in your router setup you would have the following:
const routes: Routes = [
{
path: 'my-route-path',
component: MyComponent,
resolve: {
data: MyResolver
}
}
];
Resolver
Finally, your resolver would make your API call utilizing your service:
#Injectable({providedIn: 'root'})
export class MyResolver() implements Resolve<T> {
constructor(private service: MyService) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<T> | Promise<T> | any {
return this.service.myRequest();
}
}
The final result will be that your view will not be rendered until your data is ready.
Trying to filter a collection of entities (typical blog post) by some values, using lodash filter function.
I'm developing with Vue.js (latest version) and I've a vuex module (post.js) with some getters:
computed: {
...mapGetters('post', ['postBySlug', 'postsWithoutCurrentPost']),
posts() {
return this.postsWithoutCurrentPost(this.slug, this.category)
},
},
I call postsWithoutCurrentPost getter with two parameters to filter blog posts (from vuex store) and get all blog posts with this.category minus current blog post (this.slug).
So, the postsWithoutCurrentPost implementation is as follow:
export const getters = {
postsWithoutCurrentPost: state => (slug, category) =>
_filter(state.posts, post => post.fields.slug !== slug && post.fields.category.fields.name === category.fields.name)
}
Inspecting Vue dev tools, I notice that I've an error
posts:"(error during evaluation)"
But this is really strange for me.
I left from this implementation:
export const getters = {
postsWithoutCurrentPost: state => (slug, category) =>
_filter(state.posts, post => post.fields.slug !== slug)
}
which works without any error obviously not taking into account filtering by category.
Why adding a condition does the method stop working? Where am I doing wrong?
I have an app that renders fixtures, results etc from my API based on a season id - this id is stored in state as a property of SeasonState:
export interface SeasonsState extends EntityState<Season> {
allSeasonsLoaded: boolean;
currentlySelectedSeasonId: number;
}
This is used by other components to determine which fixtures, results etc to fetch from the API and store in state. For example:
this.store
.pipe(
select(selectCurrentlySelectedSeason)
).subscribe(seasonId => {
this.store.dispatch(new AllFixturesBySeasonRequested({seasonId}));
this.fixtures$ = this.store
.pipe(
select(selectAllFixturesFromSeason(seasonId))
);
});
This works well, but what I'd really like is to be able to only fetch fixtures again fixtures for that particular season are not already stored in state.
I've tried creating a selector to use to conditionally load the data from the API in my effects:
export const selectSeasonsLoaded = (seasonId: any) => createSelector(
selectFixturesState,
fixturesState => fixturesState.seasonsLoaded.find(seasonId)
);
But I am unsure how to implement this / whether this is the right approach.
EDIT: using info from the answer below, I have written the following Effect, however see the comment - I need to be able to use seasonId from the payload in my withLatestFrom.
#Effect()
loadFixturesBySeason$ = this.actions$
.pipe(
ofType<AllFixturesBySeasonRequested>(FixtureActionTypes.AllFixturesBySeasonRequested),
withLatestFrom(this.store.select(selectAllFixtures)), // needs to be bySeasonId
switchMap(([action, fixtures]) => {
if (fixtures.length) {
return [];
}
return this.fixtureService.getFixturesBySeason(action.payload.seasonId);
}),
map(fixtures => new AllFixturesBySeasonLoaded({fixtures}))
);
Have your effect setup like this [I am using ngrx 6 so tested on ngrx 6; If you are using some other version then you will get an idea and adjust the code accordingly] -
#Effect() allFixturesBySeasonRequested: Observable<Action> =
this._actions$
.pipe(
//Please use your action here;
ofType(actions.AllFixturesBySeasonRequested),
//please adjust your action payload here as per your code
//bottom line is to map your dispatched action to the action's payload
map(action => action.payload ),
switchMap(seasonId => {
//first get the fixtures for the seasonId from the store
//check its value if there are fixtures for the specified seasonId
//then dont fetch it from the server; If NO fixtures then fetch the same from the server
return this.store
.pipe(
select(selectAllFixturesFromSeason(seasonId)),
//this will ensure not to trigger this again when you update the fixtures in your store after fetching from the backend.
take(1),
mergeMap(fixtures => {
//check here if fixtures has something OR have your logic to know
//if fixtures are there
//I am assuming it is an array
if (fixtures && fixtures.lenght) {
//here you can either return NO action or return an action
//which informs that fixtures already there
//or send action as per your app logic
return [];
} else {
//NO fixtures in the store for seasonId; get it from there server
return this.http.get(/*your URL to get the fixtures from the backend*/)=
.pipe(
mergeMap(res => {
return [new YourFixtureFetchedSucccess()];
}
)
)
}
})
);
})
)
Now you need to dispatch the action which fetches the fixtures for the specified seasonId from your service/component or the way your app is designed.
Hope it will give you an idea and helps in solving your problem.