Using react-virtualized InfiniteLoader - reactjs

Does react-virtualized's InfiniteLoader require seed data to function? The following is my component:
class Bookmarks extends PureComponent {
constructor(props) {
super(props);
this.loaded = {
cursor: null,
data: []
};
this._isRowLoaded = this._isRowLoaded.bind(this);
this._loadMoreRows = this._loadMoreRows.bind(this);
this._rowRenderer = this._rowRenderer.bind(this);
}
render() {
const size = this.loaded.data.length;
return (
<InfiniteLoader
isRowLoaded={this._isRowLoaded}
loadMoreRows={this._loadMoreRows}
rowCount={size}>
{({onRowsRendered, registerChild}) =>
<AutoSizer>
{({width, height}) =>
<List
ref={registerChild}
height={height}
onRowsRendered={onRowsRendered}
rowCount={size}
rowHeight={30}
rowRenderer={this._rowRenderer}
width={width}
/>}
</AutoSizer>}
</InfiniteLoader>
);
}
_isRowLoaded({index}) {
return !!this.loaded.data[index];
}
_loadMoreRows({startIndex, stopIndex}) {
fetch('/api/bookmarks').then((response) => {
return response.json();
}).then((json) => {
this.loaded = {
cursor: json.cursor,
data: this.loaded.data.push(...json.data),
};
});
}
_rowRenderer({index, key, style}) {
return (
<div key={key} style={style}>{this.loaded.data[index]}</div>
);
}
}
In render(), size is initially zero as there's no data yet, and I assume the component would call _loadMoreRows—apparently not.
This is what I thought to be the flow of logic (which is incorrect):
Component is created
_loadMoreRows is called (1st time)
When the promise returned by _loadMoreRows is resolved, check each loaded rows with _isRowLoaded
Render each row

In render(), size is initially zero as there's no data yet, and I assume the component would call _loadMoreRows
This is the key to the misunderstanding. InfiniteLoader needs to know if there is more data to potentially load. The docs have a couple of simple recipes for working with InfiniteLoader:
If you know the total number of remote records then look at this example.
If you only know if there's at least 1 more record/page to be loaded,
look at this example.

Related

Re render when data from async function is ready

I have an async function that GET the notes from network, the problem that I have is that it tries to render an empty array of data, therefore I get this error saying that item.id is undefined because the array is empty. I tried to put a condition if (data.length === 0) return <Text>No Entries</Text> but then it does not re render anything, even though when I console.log(data) I can see the data has arrived. Is there any way to re render when data has arrived, or any other way around this?
export default class NoteList extends Component {
render() {
const { data} = this.props;
return (
<View style={styles.cardView}>
<FlatList
numColons={data.length}
data={data}
renderItem={({ item: { name, content, id } }) => {
return (
<View>
<NoteCard
name={name}
content={content}
/>
</View>
);
}}
keyExtractor={(item) => item.id}
/>
</View>
);
}
}
How to prevent this:
TypeError: TypeError: undefined is not an object (evaluating 'item.id')
I also get this error, but I think it is related to the management of the first problem.
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory
leak in your application. To fix, cancel all subscriptions and asynchronous tasks in %s.%s, the componentWillUnmount method,
The problem you have, lies in the parent where data is saved in the state. Therefor you get the warning about updating the state of an unmounted component. There are a lot of different ways to fix this. One way I like (because of the readability), is using a variable for when the component mounts. A simple example:
class News extends Component {
_isMounted = false;
constructor(props) {
super(props);
this.state = {
news: [],
};
}
componentDidMount() {
this._isMounted = true;
axios
.get('https://hn.algolia.com/api/v1/search?query=react')
.then(result => {
if (this._isMounted) {
this.setState({
news: result.data.hits,
});
}
});
}
componentWillUnmount() {
this._isMounted = false;
}
render() {
...
}
}
Now when the data is set, the NoteList component will automatically get updated. However what happens when api call fails. To prevent stale data, I like to use conditional rendering. Just like you suggested:
export default class NoteList extends Component {
render() {
const { data } = this.props;
if (data) {
return (
<View style={styles.cardView}>
<FlatList
numColons={data.length}
data={data}
renderItem={({ item: { name, content, id } }) => {
return (
<View>
<NoteCard name={name} content={content} />
</View>
);
}}
keyExtractor={item => item.id}
/>
</View>
);
}
}
}
A common way to do this in React is to keep track of when data is being fetched. This can be done e.g. by having a isFetching field in your state:
// This would be your default state
this.state = {
isFetching: false
};
Then, when you fire off the request (preferably in componentDidMount) you set isFetching to true using:
this.setState({ isFetching: true });
And finally, when the data arrives, you set it to false again:
this.setState({ isFetching: false });
Now, in your render function you can do something like this:
render () {
return (
<div className="something">
<h3>Some content</h3>
{this.state.isFetching ? <LoadingComponent /> : (
<ul>
{listItems}
</ul>
)}
</div>
)
}
By using state, you don't have to worry about telling your component to do something, instead it reacts to changes in the state and renders it accordingly.

How to change attribute of a React Element

I've created a render method which adds a number of dynamically created 'InfoWindow' elements to a Class based object.
Each InfoWindow element has a unique ID and key.
I also have a number of 'Marker' elements with corresponding ids and keys.
Currently all Infowindows have a prop of 'visible={false}'
When I click a Marker a function is called which outputs the Marker ID.
I'd like to find the InfoWindow with the relevant ID and set visibility = {true}
Is there a way to find the relevant InfoWindow element using its key or ID, and then call setAttribute (or equivalent)?
I've tried searching the DOM for the ID but Google Maps doesn't render that way, so I'm thinking there must be a more React-y way to do this?
let visibilityFunction = () => {
this.changeVisibility(01);
};
changeVisibility = (e) => {
console.log(e);
//this currently outputs the ID (01)
}
render() {
return(
<Parent>
<InfoWindow
visible={false}
key={01-iw}
id={01-iw}
/>
<Marker
key={01}
id={01}
onClick={visibilityFunction}
/>
</Parent>
);
}
Like I was saying in the comments. Use state here to update the visibility.
class MyComponent extends React.Component {
state = { visibleWindows: {}, currentWindows: [1] };
changeVisibility = id => {
this.setState(prevState => ({
visibleWindows: {
...prevState.visibleWindows,
[id]: !prevState.visibleWindows[id]
}
}));
};
render() {
const { currentWindows, visibleWindows } = this.state;
return (
<div>
{currentWindows.map(win => (
<ChildWindow key={win} id={win} isVisible={!!visibleWindows[win]} onChange={this.changeVisibility} />
))}
</div>
);
}
}
class ChildWindow extends React.Component {
changeVisibility = () => {
this.props.onChange(this.props.id)
}
render() {
<React.Fragment>
<InfoWindow
visible={this.props.isVisible}
key={`${win}-iw`}
id={`${win}-iw`}
/>
<Marker
key={win}
id={win}
onClick={this.changeVisibility}
/>
</React.Fragment>
}
}
Here's a rudimetary example for you to poke around with :)

How to structure React code with Apollo Query so that a random queried array of objects doesn't rerender with state change?

The problem: I'm using Apollo Client, and have the deck rendered like this "/deck/2" and I want to randomly shuffle the cards, and display just one at a time with a button to view the next. I keep running in the problem with React re-rendering everytime the state is changed (my onClick index counter +1), which reshuffles the cards since the shuffledCards variable is inside the query. I'm not sure how to prevent this from happening.
How can I shuffle the list without worrying about them being reshuffled 'onClick' of the button. I imagine there is a way to get the randomized array outside of the render, which I can do in Regular react, but using Apollo queries I'm stumbling to understand.
This is where I am stumbling due to my inexperience in React and Graphql with Apollo, and I haven't found a similar Apollo graphql project to lean off of. I can't use map on an object, but maybe there is a way to use map to display 1 object of the array at a time? I haven't found a working solution.
What I intend to have happen: I simply want to render the shuffled array of cards one at a time, and pressing the next button should step through the cards in the randomized array, without re-rendering whenever I click the button, otherwise cards will be repeated at random.
Here's the code:
import React, { Component, Fragment } from "react";
```
import CardItem from "./CardItem";
const CARDS_QUERY = gql`
query CardsQuery($id: ID!) {
```
`;
export class Cards extends Component {
constructor(props) {
super(props);
this.state = {
index: 0
};
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
this.setState({
index: this.state.index + 1
});
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
render() {
let { id } = this.props.match.params;
id = parseInt(id);
return (
<Fragment>
<Query query={CARDS_QUERY} variables={{ id }}>
{({ data, error, loading }) => {
if (loading) {
return <Loading />;
}
if (error)
}
const CardsToRender = data.deck.cards;
//This variable gets reshuffled at every re-render
const shuffledCards = this.shuffle(CardsToRender);
//Single item to be returned
const item = shuffledCards[this.state.index];
if (this.state.index >= shuffledCards.length) {
return (
<div>
<h1>Finished</h1>
</div>
);
} else {
return (
<Fragment>
// Here I can get one item to display, but if I press next, the state changes which fires a re-render,
//shuffling the cards once more. My intention is to only shuffle cards at first render until the browser page is
//refreshed or user navigates away
<h1>{item.front}</h1>
<h1>{item.back}</h1>
//My second attempt is to map out the cards, but I have only been able to render a list,
// but not one at a time. Maybe there is a simple code solution in my .map to display
//one at a time without needing to change state?
{shuffledCards.map(card => (
<CardItem key={card.id} card={card} />
))}
<p>
<button onClick={this.goToNext}>Next</button>
</p>
</Fragment>
);
}
}}
</Query>
</Fragment>
);
}
}
```
I'll be grateful for any help provided. Thank you!
I am not aware of Appolo Query but the issue you mentioned is more related to React. You can try below one to avoid shuffling cards on each re-render.
Refactor your component into two parts.
1) ShuffleCards.js(give any name as you like :) ) - Move your component into "ShuffleCards" and pass shuffled cords to the child component where you can update the state to render the next card.
// ShuffledCards.js
import React, { Component, Fragment } from "react";
```
import CardItem from "./CardItem";
const CARDS_QUERY = gql`
query CardsQuery($id: ID!) {
```
`;
export class ShuffleCards extends Component {
constructor(props) {
super(props);
}
shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
render() {
let { id } = this.props.match.params;
id = parseInt(id);
return (
<Fragment>
<Query query={CARDS_QUERY} variables={{ id }}>
{({ data, error, loading }) => {
if (loading) {
return <Loading />;
}
if (error) {
}
const CardsToRender = data.deck.cards;
const shuffledCards = this.shuffle(CardsToRender);
return (
<Cards shuffledCards={shuffledCards} />
);
}}
</Query>
</Fragment>
);
}
}
Move the code which handles the displaying card and updating the state to the "Cards" component.
import React, { Component, Fragment } from "react";
import CardItem from "./CardItem";
export class Cards extends Component {
constructor(props) {
super(props);
this.state = {
index: 0
};
this.goToNext = this.goToNext.bind(this);
}
goToNext() {
this.setState({
index: this.state.index + 1
});
}
render() {
const {shuffledCards} = this.props || [];
return (
<div>
{
this.state.index >= shuffledCards.length ?
<div>
<h1>Finished</h1>
</div>
:
<Fragment>
<h1>{item.front}</h1>
<h1>{item.back}</h1>
{
shuffledCards.map(card => (
<CardItem key={card.id} card={card} />
))
}
<p>
<button onClick={this.goToNext}>Next</button>
</p>
</Fragment>
}
</div>
)
}
}
You are calling this.shuffle() in your render function - therefore it will shuffle on each render.
Move that to your constructor and it will only get called once.
constructor(props) {
super(props);
this.state = {
index: 0
};
this.goToNext = this.goToNext.bind(this);
const CardsToRender = data.deck.cards;
}

How to scroll to bottom when props changed in react-virtualized?

I have component App with List from react-virtualized library.
And I need on initial render, that my List scroll to bottom.
And I did it, when added scrollToIndex option. But when I add new object in my list array, it does not scroll to my last added object. How can I fix it? And is it good solution to use "forceUpdate()" function?
import { List } from "react-virtualized";
import loremIpsum from 'lorem-ipsum';
const rowCount = 1000;
const listHeight = 600;
const rowHeight = 50;
const rowWidth = 800;
class App extends Component {
constructor() {
super();
this.renderRow = this.renderRow.bind(this);
this.list = Array(rowCount).fill().map((val, idx) => {
return {
id: idx,
name: 'John Doe',
image: 'http://via.placeholder.com/40',
text: loremIpsum({
count: 1,
units: 'sentences',
sentenceLowerBound: 4,
sentenceUpperBound: 8
})
}
});
}
handle = () => {
this.list = [...this.list, { id: 1001, name: "haha", image: '', text: 'hahahahahaha' }];
this.forceUpdate();
this.refs.List.scrollToRow(this.list.length);
};
renderRow({ index, key, style }) {
console.log('____________', this.list.length);
return (
<div key={key} style={style} className="row" >
<div className="image">
<img src={this.list[index].image} alt="" />
</div>
<div onClick={this.handle}>{this.state.a}</div>
<div className="content">
<div>{this.list[index].name}</div>
<div>{this.list[index].text}</div>
</div>
</div>
);
}
render() {
return (
<div className="App">
<div className="list">
<List
ref='List'
width={rowWidth}
height={listHeight}
rowHeight={rowHeight}
rowRenderer={this.renderRow}
rowCount={this.list.length}
overscanRowCount={3}
scrollToIndex={this.list.length}
/>
</div>
</div>
);
}
}
export default App;
You mentioning you need to scroll to the bottom when the list item is changed and to be honest i don't like to use forceUpdate. As mentioned on the React docs:
Normally you should try to avoid all uses of forceUpdate() and only read from this.props and this.state in render().
Luckily, one of React lifecycle method is suitable for this case, it is call componentDidUpdate. But you need to do some refactor of your code. Instead using private field, i suggest to put it on state/props.
This method will invoked immediately after updating props/state occurs. However, This method is not called for the initial render.
What you need to do is, compare the props, is it change or not? Then call this.refs.List.scrollToRow(this.list.length);
Sample code
class App extends Component {
constructor() {
this.state = {
list: [] // put your list data here
}
}
// Check the change of the list, and trigger the scroll
componentDidUpdate(prevProps, prevState) {
const { list } = this.state;
const { list: prevList } = prevState;
if (list.length !== prevList.length) {
this.refs.List.scrollToRow(list.length);
}
}
render() {
// usual business
}
}
more reference for React lifecyle methods:
https://reactjs.org/docs/react-component.html#componentdidupdate

Why Relay Modern is making new requests, instead of using cache

I have a page where you can see the current Item and click "Next" to see the next one. Here is how this component looks like:
class App extends Component {
constructor(props) {
super(props);
this.state = {
index: 0,
ids: ["VXNlcjox", "VXNlcjoy"]
};
this.onNext = () => this.setState(s => ({ index: (s.index + 1) % 2 }));
}
render() {
const { index, ids } = this.state;
return (
<div>
<button type="button" onClick={this.onNext}>
next
</button>
<QueryRenderer
environment={environment}
query={graphql`
query App_Query($id: ID!) {
node(id: $id) {
id
}
}
`}
variables={{ id: ids[index] }}
render={({ error, props }) => {
if (error) {
return <div>Error!</div>;
}
if (!props) {
return <div>Loading...</div>;
}
return <pre>{JSON.stringify(props, null, 2)}</pre>;
}}
/>
</div>
);
}
}
What I expect, is that each Item will be fetched only when requested for the first time and later used from cache.
But what I see, is that the new request made in the network tab every time I click "next", even if this Item was requested before. If I open Relay DevTools - Items with this id is already in the store:
So why is the new request made every time? Isn't Relay Modern supposed to reuse previously cached data?
You can pass the QueryRenderer one of these dataFrom props:
NETWORK_ONLY – return to the network each time.
STORE_THEN_NETWORK – attempt to render from the store, then update the store with fresh data from the network.
Try this:
<QueryRenderer
dataFrom="STORE_THEN_NETWORK"
...
/>
See the implementation: https://github.com/facebook/relay/blob/master/packages/react-relay/modern/ReactRelayQueryRenderer.js#L297-L299
I don't think it's the way query renderer is meant to work by default.
You're probably looking for this relay-query-lookup-renderer

Resources