JavaScript / React - Async array map - javascript

I am using React Native. I want to create a very long list.
Each element in the List component is a ListItem component.
My problem is that the component is loading too much time. The List is created with Array.map().
Please have a look at the code.
My question is whether it is somehow possible to create async map() so elements in the list will load one by one and the component won't wait that much?
Tried something like this but nothing works for me. Can someone help please?
class List extends React.Component {
render() {
// very big array
var list = [...Array(5000).keys()];
return (
<Content>
{
list.map((item, index) => {
return (
<ListItem text={item} key={index}/>
);
})
}
</Content>
);
}
}
class ListItem extends React.Component {
render() {
return (
<Button>
<Text>{this.props.text}</Text>
</Button>
);
}
}

The async map code you linked to works, it just doesn't do what you want it to. The time it takes to render 5000 components from your array still takes a long time, async will just allow you to asynchronously do stuff on its completion.
From a UX perspective, you almost never need to render that many elements. Instead, you should load the data in as you need it (user scrolls down to a certain point).
Option 1
So the data flow logic would look something like:
Load first 100 entries to display.
User needs more entries
(scrolls down the list further) so you make a request
Load the
next 100 entries to display.
This is a common pattern if you are using an API to interface with your data.
Option 2
If you have the array in memory, you could try dividing the array into smaller chunks with Array.slice() and rendering each of those asynchronously.

Using async wont fix the issue. The size of array is big and mapping means running a for loop so for loop for let's say 5000 items will take time.
Restrict the size of array or increment them in bundles of 100 as you
go.

Related

How do I show an activity indicator every time my flatlist data changes?

I'm setting the data that my flatlist component displays using a state called selectedStream. selectedStream changes every time the user presses a different group option. I've noticed that the flatlist takes 1-3 seconds to refresh all the posts that it's currently displaying already. I want there to be a loading indicator so that by the time the indicator goes away, the list is already properly displayed with the newly updated data.
<FlatList
maxToRenderPerBatch={5}
bounces={false}
windowSize={5}
ref={feedRef}
data={selectedStream}/>
Whenever we are working with anything related to the UI, sometimes we may face delays in UI re-rendering. However, we need to first figure out what is actually causing the delay.
The right question to ask about your code would be:
Is the rendering of items taking longer than expected? Or, is the data being passed with a delay because it is dependant on an API call or any other async task?
Once you answer that question, you may end up with two scenarios:
1. FlatList taking longer to render views
This doesn't usually happen as the RN FlatList will only render views that are visible to the user at any given time and will keep rendering new views as the user scrolls through the list. However, there may be some flickering issues for which you can refer to the below article:
8 Ways to optimise your RN FlatList
2. Passing the data causes the delay
This is the most common scenario, where we may call an API endpoint and get some data and then do setState to update any view/list accordingly. A general approach is to show some sort of a progress-bar that would indicate that the application is busy and thus maintaining a proper user-experience. The easiest way to do that is by conditional rendering.
A general example would be:
const [myList, setMyList] = useState();
function callAPIforMyList(){
// logic goes here
}
return {
{myList ? <ActivityIndicator .../> : <Flatlist .... />
}
The above code will check if myList is undefined or has a value. If undefined, it will render the ActivityIndicator or else the FlatList.
Another scenario could be when myList may have existing data but you need to update/replace it with new data. This way the above check may fail, so we can put another check:
const [myList, setMyList] = useState();
const [isAPIbusy, setAPIBusy] = useState(false)
function callAPIformyList() {
setAPIBusy(true)
/// other logics or async calls or redux-dispatch
setAPIBusy(false)
}
return {
{!isAPIBusy && myList ? (<Flatlist .... />) : (<ActivityIndicator .../>)
}
You can add multiple conditions using more turneries such as isAPIBusy ? <View1> : otherBoolean ? <View2> : <Default_View_When_No_Conditions_Match)/>
Hope this helps clarify your needs.

Using Linked Lists in Checkerbox selection List react-native

I created a Linked-List data structure in my react-native app that I want to move between screens and then pick a node based on a checkerbox selection menu.
I understand that I can move the list using react-native-navigation, so now I would like to display the list with a checkerbox list to select multiple nodes and perform actions on them. The problem I see is that checkerbox lists use defined const arrays of items that are then listed.
The whole reason I went with linked-lists is that I need the list to dynamically update. (It may be more beneficial to use an array of large size instead, but each element within the node is somewhat large and I am unsure what effect a large array would have.)
Is there a way to input a linked list into a checkerbox list or would I have to create an array?
How would I go about either option if they need to dynamically update?
It may be more beneficial to use an array of large size instead, but each element within the node is somewhat large and I am unsure what effect a large array would have.
JavaScript arrays only hold references to the objects they store since it is a dynamic scripting language supporting prototype based object construction. Because of this, each element's size will not affect the array performance.
Another possibility is to extend the built-in Array class in your Linked-List data structure to ensure compatibility.
Your decision on using an Array or a Linked-List should be based on the List operations your app uses the most. There are a lot of articles about that.
Is there a way to input a linked list into a checkerbox list or would
I have to create an array?
How would I go about either option if they need to dynamically update?
There are some git repositories that add support for what you want to achieve (here is an example, feel free to explore npm for more).
Another possibility, if you want your items to dynamically update, will be to encapsulate them in a React.Component for rendering:
// YourNode.js
import React, { Component } from 'react';
import { CheckBox } from 'react-native';
export default class YourNode extends Component {
constructor(props) {
super(props);
this.state = {
checked: false,
};
}
selectItem = () => {
const { onPress } = this.props;
// Update the state
this.setState({ checked: !this.state.checked });
onPress();
};
render() {
const { node } = this.props;
// You may want to render more things like `Text` or `View` components based on
// the node's content
return (
<CheckBox
value={this.state.checked}
onValueChange={this.selectItem}
/>
);
}
}
import YourNode from './YourNode';
// YourScreen.js
render() {
//...
// This will render a `YourNode` component for each one of your nodes.
{yourList.map((item, index)) => {
return (<YourNode node={item} onPress{() => this.selectedNodes.push(item)}>)
}
}
}

How to "go back" on paginated items

I have a simple collection which I would like to paginate.
I want to sort it and paginate it by a timestamp doc called createdAt.
This is how the call currently looks like:
function getPaginatedItems (db, startAfter) {
return db
.collection('items')
.orderBy('createdAt')
.startAfter(startAfter) // startAfter parameter will be a createdAt Timestamp doc
.limit(3)
.get()
}
To make this easier to work with and display, I created a function that will turn this query snapshot into a paginated object. This looks something like this:
function querySnapshotToPaginatedObject (querySnapshot, total, limit = 3) { if (querySnapshot.empty) {
return {
total: 0,
limit,
data: []
}
} else {
return {
total,
limit,
data: querySnapshot.docs.map(doc => ({
id: doc.id,
...doc.data()
}))
}
}
}
As it stands I have a total of 11 items in my firestore, but would like to get them in chunks of three. This all works perfectly when moving forward with the data, however my question then becomes, how do I go back? That is, how do I get data from the previous pages?
Currently what I have in my hands is the total number of items I have, the limit which can be displayed and obviously the three items I am currently displaying.
I have no idea how to keep track of all other ones in order to jump back pages, or jump more than one page for that matter.
So I guess there are two questions here: how do I go back to previous data? And how could I jump different chunks of data?
Is there another way to do this, perhaps by index instead of a specific doc (like I am doing with createdAt)?
Edit: I was asked how I am making my next queries. Basically I have buttons (all with their page numbers) and when I click on them, I do a second call starting with the createdAt attribute of the last item. I then do a second call to my initial query, but passing in the last object as the startAfter parameter in the getPaginatedItems function call.
I am using react as the front-end, so it looks something like this:
getNextBatch (startAfter) {
return {
paginatedItems: querySnapshotToPaginatedObject(
await getPaginatedItems(db, startAfter), 11, 3
)
}
}
...
export default class MyComponent extends React.Component (
render () {
return (
<div>
{this.props.paginatedItem.map(x => <div>{x.name} {x.createdAt}</div>)}
<button onClick={(evt) => console.warn('How do I go back???')}>
Back
</button>
<button onClick={(evt) => getNextBatch(paginatedItem[paginatedItem - 1].createdAt)}>
Next
</button>
</div>
)
}
)
Keep in mind that the component does re-render every time I click the buttons.
Cloud Firestore APIs don't provide a way to page backward. The easiest thing to do in your case is to remember a list of startAfter values that you've used to fetch each page. Each value will represent a page of data. With that, going back to a previous page is just a matter of find the desired startAfter value from that list, then making the query with that.
To be honest, though, your total data set is pretty small, and it's probably not worth paging at all. I'd just get the whole thing and keep it in memory. Paging probably don't become worthwhile until you reach hundreds or thousands of documents (depending on how big each document is, of course, and how much memory you expect to have available).

React is rerendering my list even though each child in array has its unique key

So, as far as I understand react only rerenders new elements with new keys. Thats not working for me though.
I have a list of posts, that are limited to 3.
When the user scrolls to bottom of page I add 3 to the limit, which means at the bottom of the page 3 older posts are supposed to be shown.
What I have now works, but the entire list is being rerendered. And it jumps to the top which is also not wanted (this I can fix though, main problem is the rerendering). They all have unique keys. How can I prevent this behaviour?
thisGetsCalledWhenANewPostComesIn(newPost){
let newPosts = _.clone(this.state.posts);
newPosts.push(newPost);
newPosts.sort((a,b) => b.time_posted - a.time_posted);
this.setState({posts: newPosts});
}
render(){
return (
<div ref={ref => {this.timelineRef = ref;}} style={styles.container}>
{this.state.posts.map(post =>
<Post key={post.id} post={post} />
)}
</div>
);
}
Having unique keys alone does not prevent rerendering components that have not changed. Unless you extend PureComponent or implement shouldComponentUpdate for the components, React will have to render() the component and compare it to the last result.
So why do we need keys when it's really about shouldComponentUpdate?
The purpose of giving each component in a list a unique key is to pass the props to the "right" component instances, so that they can correctly compare new and old props.
Imagine we have a list of items, e.g.:
A -> componentInstanceA
B -> componentInstanceB
C -> componentInstanceC
After applying a filter, the list must be rerendered to show the new list of components, e.g.:
C -> ?
Without proper unique keys, the component that previously rendered A will now receive the prop(s) for C. Even if C is unchanged, the component will have to rerender as it received completely different data:
C -> componentInstanceA // OH NO!
With proper unique keys, the component that rendered C will receive C again. shouldComponentUpdate will then be able to recogize that the render() output will be the same, and the component will not have to rerender:
C -> componentInstanceC
If your list of items take a long time to render, e.g. if it's a long list or each element is a complex set of data, then you will benefit from preventing unnecessary rerendering.
Personal anecdote
In a project with a list of 100s of items which each produced 1000s of DOM elements, changing from
list.map((item, index) => <SomeComp key={index} ... />)
to
list.map(item => <SomeComp key={item.id} ... />)
reduced the rendering time by several seconds. Never use array index as key.
You will have to implement shouldComponentUpdate(nextProps, nextState) in the Post component. Consider extending the PureComponent class for the Post component instead of the default React Component.
Good luck!
PS: you can use a string as ref parameter for your div in the render method like so:
render() {
return (
<div
ref='myRef'
style={styles.container}
>
{this.getPostViews()}
</div>
);
}
Then, if you want to refer to this element, use it like this.refs.myRef. Anyway, this is just a personal preference.
Okay, my bad. I thought I'd only post the "relevant" code, however it turns out, the problem was in the code I left out:
this.setState({posts: []}, ()=> {
this.postListenerRef = completedPostsRef.orderByChild('time')
.startAt(newProps.filter.fromDate.getTime())
.endAt(newProps.filter.toDate.getTime())
.limitToLast(this.props.filter.postCount)
.on('child_added', snap => {
Database.fetchPostFromKey(snap.key)
.then(post => {
let newPosts = _.clone(this.state.posts);
newPosts.push(_.assign(post, {id: snap.key}));
newPosts.sort((a,b) => b.time_posted - a.time_posted);
this.setState({posts: newPosts});
}).catch(err => {throw err;});
});
});
I call setState({posts: []}) which I am 99% sure is the problem.

How to update Baobab leaf data before React component render?

ReactJS, Baobab, Material-UI app displays some items, identified by their numeric id. To display those, title and image url's are retrieved from a remote service via ajax. Tree branch stores that data:
data: {
12345: {title:'ABC', image:'https://...'}, // id is 12345
12346: {...
}
Upon item component creation and first rendering, its extended data may, or may not be already available in the tree. If its not, ajax call is enqueued to receive that data. It might happen that multiple items are created with the same item id.
To avoid extra requests for the same id, I want to put a dummy info {title:'loading', image:'spinner.gif'} into the tree upon the first request to that id's info. Thus this data will be used for the very first render(). Successive components would get that dummy info, and will not initiate any extra requests.
Question: how, and where can I place the code to test if the tree has no info yet and place the dummy there to indicate its "penging" state and enqueue the request?
Tried so far:
component's constructor – props are not set there yet;
componentWillMount() – the first render started with the old state of the tree, despite the tree.commit() after setting the dummy value;
in the branch function that dynamically creates components cursor pointing to its data. Got warning:
setState(...): Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
This can be solved one level up – once the list of ids is available. But it feels right that a component should be able to handle its data within itself.
Please advice a correct way to immediately update Baobab tree data before the first render of a React Component, from within that Component?
In my case (i am use same stack) wrap branch work fine.
import BaobabPropTypes from 'baobab-react/prop-types';
class Actions {
/**
* #param {Baobab} tree
*/
static prefetchTree = (tree) => {
tree.select(somePath).set(defaultValue);
tree.commit();
};
}
class Page extends React.Component {
static contextTypes = {
tree: BaobabPropTypes.baobab
};
componentWillMount() {
Actions.prefetchTree(this.context.tree);
}
render() {
return <Branch {...this.props}/>;
}
}
Baobab has a get event, use it to detect requests that return values that are not fetched yet:
tree.on('get', function(e) {
if (e.data.data === undefined) {
const path = e.data.path; // requested cursor path like ['data',12345]
const id = path[1];
FETCH_DATA(id)
.then( data => tree.set(path , data) );
tree.set(path, PLACEHOLDER_DATA);
}

Categories