RecyclerListView scrolls to top onEndReached with functional component - reactjs

I have implemented a RecyclerListView(Flipkart Github) using Redux as seen below. Everything seems to be working great except when onEndReached is called and a new batch of data comes through, the list gets positioned to the top of the page rather than remaining smooth. See that behavior in the GIF below:
Note: This is happening on web(chrome). I tried the latest stable and 2.0.13-alpha.1
import React, { useCallback } from 'react';
import { View, Text, Dimensions } from 'react-native';
import { useSelector, useDispatch } from 'react-redux';
import { RecyclerListView, DataProvider, LayoutProvider } from 'recyclerlistview/web';
import { createSelector } from 'reselect';
import { loadData } from '../actions';
const selectData = createSelector(
state => state.data,
data => Object.values(data),
);
let containerCount = 0;
class CellContainer extends React.Component {
constructor(args) {
super(args);
this._containerId = containerCount++;
}
render() {
return (
<View {...this.props}>
{this.props.children}
<Text>Cell Id: {this._containerId}</Text>
</View>
);
}
}
const List = ({ isServer }) => {
let { width } = Dimensions.get('window');
const dispatch = useDispatch();
const data = useSelector(selectData);
const dataDataProvider = new DataProvider((r1, r2) => {
return r1 !== r2;
}).cloneWithRows(data);
const onEndReached = useCallback(() => dispatch(loadData()), [dispatch]);
const layoutProvider = new LayoutProvider(
() => 0,
(type, dim) => {
dim.width = width;
dim.height = 240;
},
);
const rowRenderer = (type, data) => {
return <CellContainer />;
};
return (
<View
style={{
display: "flex",
flex: 1,
width: "100vw",
height: "100vh"
}}
>
<RecyclerListView
layoutProvider={layoutProvider}
dataProvider={dataDataProvider}
onEndReached={onEndReached}
rowRenderer={rowRenderer}
/>
</View>
);
};
export default List;
UPDATE: I can confirm that the class-based version works with no issues using redux connect. Leaning towards this being some kind of incompatibility with the library. Interesting nonetheless.
Below snippet is a simplified working example of this demo https://codesandbox.io/s/k54j2zx977
class App extends React.Component {
constructor(props) {
let { width } = Dimensions.get('window');
super(props);
this.state = {
dataProvider: new DataProvider((r1, r2) => {
return r1 !== r2;
}),
layoutProvider: new LayoutProvider(
index => 0,
(type, dim) => {
dim.width = width;
dim.height = 240;
},
),
count: 0,
};
}
componentDidUpdate(prevProps) {
if (this.props.data.length !== prevProps.data.length) {
this.setState({
dataProvider: this.state.dataProvider.cloneWithRows(this.props.data),
count: this.props.data.length
});
}
}
componentWillMount() {
this.props.loadData();
}
async fetchMoreData() {
this.props.loadData();
}
rowRenderer = (type, data) => {
//We have only one view type so not checks are needed here
return <CellContainer />;
};
handleListEnd = () => {
this.fetchMoreData();
//This is necessary to ensure that activity indicator inside footer gets rendered. This is required given the implementation I have done in this sample
this.setState({});
};
render() {
//Only render RLV once you have the data
return (
<View style={styles.container}>
{this.state.count > 0 ? (
<RecyclerListView
style={{ flex: 1 }}
contentContainerStyle={{ margin: 3 }}
onEndReached={this.handleListEnd}
dataProvider={this.state.dataProvider}
layoutProvider={this.state.layoutProvider}
renderAheadOffset={0}
rowRenderer={this.rowRenderer}
/>
) : null}
</View>
);
}
}
export default connect(
state => ({
data: selectData(state),
}),
dispatch => bindActionCreators({ loadData }, dispatch),
)(App);

Just add the below line of code after your layoutProvider. The shouldRefreshWithAnchoring property of layoutProvider prevents recycler view list to not refresh the existing rows if more rows are added to the list.
layoutProvider.shouldRefreshWithAnchoring = false;

Solution is here:
https://github.com/Flipkart/recyclerlistview/issues/449
Just add
const [layoutProvider] = React.useState(
new LayoutProvider(
(index) => 1,
(type, dim) => {
dim.width = ITEM_HEIGHT
dim.height = ITEM_HEIGHT
}
)
)

Related

React (react-native-modalize) not working with flatListProps object

Is anyone using react-native-modalize module?
react-native-modalize module, when i render code on flatListProps object its showing the error below!!
Here is the example as well https://jeremybarbet.github.io/react-native-modalize/#/EXAMPLES
import React, { useRef } from 'react';
import { View, Text, TouchableOpacity } from 'react-native';
import { Modalize } from 'react-native-modalize';
export const App = () => {
const modalizeRef = useRef<Modalize>(null);
const onOpen = () => {
modalizeRef.current?.open();
};
const arrayData = [ { "heading": "test"}, {"heading": "test2"}... ]
let data = { "object": [arrayData] }
const getData = () => ({ data });
const renderItem = (item) => (
<View>
<Text>{item.heading}</Text>
</View>
);
return (
<>
<TouchableOpacity onPress={onOpen}>
<Text>Open the modal</Text>
</TouchableOpacity>
<Modalize
ref={modalizeRef}
flatListProps={{
data: getData(),
renderItem: renderItem,
keyExtractor: item => item.heading,
showsVerticalScrollIndicator: false,
}}
/>
</>
);
}
data of flatListProps is an array object.
let data = { "object": [arrayData] }
const getData = () => ([ ...data.object ]);

Using typed.js with React function components

typed.js doesn't offer an example for this in the docs, only for class components:
class TypedReactDemo extends React.Component {
componentDidMount() {
const options = { ... };
this.typed = new Typed(this.el, options );
}
render() {
return (
<span ref={(el) => { this.el = el; }} />
);
}
}
import React, { useRef, useEffect } from "react";
import Typed from "typed.js";
const Example = () => {
const typeTarget = useRef(null);
useEffect(() => {
const typed = new Typed(typeTarget.current, {
strings: ["<i>First</i> sentence.", "& a second sentence."],
typeSpeed: 40,
});
return () => {
typed.destroy();
};
}, []);
return <span ref={typeTarget} />;
};
export default Example;

Getting "Can't perform a React state update on an unmounted component" when switching between screens

When switching between the Home.js and Chat.js files, I get this 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 the componentWillUnmount method". I removed the only listener that's on Chat.js and tried only setting state when the component is mounted in Home.js and removing it on unmount but I still get this warning.
Home.js
import React, { Component } from "react";
import { View, FlatList } from "react-native";
import { ListItem } from "react-native-elements";
import fireStoreDB from "../database/FirestoreDB";
let _isMounted = false;
export default class Home extends Component {
constructor(props) {
super(props);
this.state = {
usersInfo: [],
refreshing: false
};
}
componentDidMount() {
_isMounted = true;
this.LoadUsers();
}
componentWillUnmount() {
_isMounted = false;
}
LoadUsers = () => {
fireStoreDB
.getAllUsersExceptCurrent()
.then(users =>
Promise.all(
users.map(({ id, username, avatar }) =>
fireStoreDB
.getUserLastMessage(fireStoreDB.getUID, id)
.then(message => ({ id, username, avatar, message }))
)
)
)
.then(users => {
if (_isMounted) {
this.setState({
usersInfo: users.filter(x => typeof x.avatar !== "undefined"),
refreshing: false
});
}
});
};
renderItem = ({ item }) => (
<ListItem
onPress={() => {
this.props.navigation.navigate("Chat", {
userTo: item.id,
UserToUsername: item.username,
LoadUsers: this.LoadUsers
});
}}
title={item.username}
subtitle={item.message}
leftAvatar={{ source: { uri: item.avatar } }}
bottomDivider
chevron
/>
);
render() {
return (
<View>
<FlatList
data={this.state.usersInfo}
renderItem={this.renderItem}
keyExtractor={item => item.id}
refreshing={this.state.refreshing}
onRefresh={() => {
this.setState({ refreshing: true });
this.LoadUsers();
}}
/>
</View>
);
}
}
Chat.js
import React, { Component } from "react";
import { View, KeyboardAvoidingView } from "react-native";
import { HeaderBackButton } from "react-navigation-stack";
import { GiftedChat } from "react-native-gifted-chat";
import * as Progress from "react-native-progress";
import fireStoreDB from "../database/FirestoreDB";
const Themes = {
primaryTheme: "#30D921",
secondaryTheme: "#B32D83",
layoutTheme: "#c0c0c0"
};
export default class Chat extends Component {
static navigationOptions = ({ navigation }) => ({
title: navigation.getParam("UserToUsername"),
headerLeft: (
<HeaderBackButton
onPress={() => {
navigation.state.params.LoadUsers();
navigation.goBack();
}}
/>
)
});
constructor(props) {
super(props);
this.state = {
messages: [],
userToId: this.props.navigation.getParam("userTo")
};
}
componentDidMount() {
fireStoreDB.getMessages(
message =>
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, message)
})),
this.chatId
);
}
componentWillUnmount() {
fireStoreDB.removeSnapshotListener(this.chatId);
}
// gifted chat user props
get user() {
return {
_id: fireStoreDB.getUID,
name: fireStoreDB.getName,
avatar: fireStoreDB.getAvatar
};
}
// merge ids between two parties for one to one chat
get chatId() {
const userFromId = fireStoreDB.getUID;
const chatIdArray = [];
chatIdArray.push(userFromId);
chatIdArray.push(this.state.userToId);
chatIdArray.sort(); // prevents other party from recreating key
return chatIdArray.join("_");
}
render() {
if (this.state.messages.length === 0) {
return (
<View
style={{
alignItems: "center",
marginTop: 260
}}
>
<Progress.Bar indeterminate color={Themes.primaryTheme} />
</View>
);
}
return (
<View style={{ flex: 1 }}>
<GiftedChat
messages={this.state.messages}
onSend={messages => fireStoreDB.sendMessages(messages, this.chatId)}
user={this.user}
/>
<KeyboardAvoidingView behavior="padding" keyboardVerticalOffset={80} />
</View>
);
}
}
FirestoreDB.js
removeSnapshotListener = chatId => {
firebase
.firestore()
.collection("messages")
.doc(chatId)
.collection("chats")
.orderBy("createdAt")
.onSnapshot(() => {});
};
UPDATE:
With your implementation, you cannot unsubribe messages collection.
You could try to return unsubscribe function from getMessages, then use it in componentWillUnmount
FirestoreDB.js
getMessages = (callback, chatId) => {
return firebase
.firestore()
.collection("messages")
.doc(chatId)
.collection("chats")
.orderBy("createdAt")
.onSnapshot(callback);
}
Chat.js
componentDidMount() {
this.unsubcribe = fireStoreDB.getMessages(
message =>
this.setState(previousState => ({
messages: GiftedChat.append(previousState.messages, message)
})),
this.chatId
);
}
componentWillUnmount() {
this.unsubcribe();
}

React ref assignment inconsistencies when using a mapping function?

I'm setting up a virtualized scrolling component in React, and using refs with a recycled observer to notify the app when to prepare another batch of data. Inside my Grid component, I map over the current batch of data and assign a ref to a sentinel div, except that ref returns null in componentDidMount(). I don't understand why since componentDidMount fires after render executes, so the reference should be available.
The only workaround to this I've found is using this janky solution in my componentDidMount: setTimeout(() => this.observer.observe(this.targetRef.current), 0);.
import React, { Component, createRef } from "react";
class Grid extends Component {
constructor(props) {
super(props);
this.state = {
batch: []
};
this.observer = null;
this.targetRef = null;
this.lastRowFirstVisible =
props.rowCount * props.columnCount - props.columnCount;
this.config = {
rootMargin: "0px",
threshold: 1
};
this.setTargetRef = element => {
this.targetRef = element;
};
}
componentDidMount() {
const { startIndex, numberToDisplay } = this.props;
this.setBatch(startIndex, numberToDisplay);
this.observer = new IntersectionObserver(function(entries, self) {
entries.forEach(entry => {
if (entry.isIntersecting) {
console.log(entry);
// self.unobserve(entry.target);
}
});
}, this.config);
setTimeout(() => this.observer.observe(this.targetRef), 0);
}
setBatch = (startIndex, numberToDisplay) => {
const batch = this.getBatch(startIndex, numberToDisplay);
this.setState({ batch });
};
getBatch = (startIndex, numberToDisplay) => {
const { data } = this.props;
return data.slice(startIndex, numberToDisplay);
};
// TO DO
updateObserver = () => {
this.observer.observe(this.targetRef.current);
};
render() {
const { lastRowFirstVisible } = this;
const { batch } = this.state;
const { elementWidth, elementHeight } = this.props;
console.log(lastRowFirstVisible);
return (
<>
{batch.map((element, localIndex) => {
const { index } = element;
console.log(localIndex === lastRowFirstVisible);
return localIndex === lastRowFirstVisible ? (
<div
id={index}
key={index}
style={{ width: elementWidth, height: elementHeight }}
className="card"
ref={this.setTargetRef}
>
{this.props.renderRow(element)}
</div>
) : (
<div
key={index}
style={{ width: elementWidth, height: elementHeight }}
className="card"
>
{this.props.renderRow(element)}
</div>
);
})}
</>
);
}
}
export default Grid;
Expected results: render function finishes executing, assigns DOM node to this.targetRef for use in componentDidMount()
Actual results: this.targetRef is still null in componentDidMount()

React with Redux: Child component does not rerender after props have changed (even though they are not shallow equal)

I'm building an app with React Native using Redux for the state management. I will post my code for all the involved components and the reducer down below, but since that is going to be much, let me describe the problem in a few sentences first.
I have an immutable reducer for my objects called 'waitercalls'. I have a screen (HomeScreen) that renders two components. Each component is a <FlatList /> of objects. The objects (waitercalls) are given to them via props by it's parent (HomeScreen). HomeScreen is connected to Redux via React-Redux' connect() and gets the objects ('waitercalls') via a selector created with Re-Select.
The left list's items can be pressed and upon press dispatch an event to the reducer. Here comes the problem. When the left list's item are pressed, the left list correctly updates (calls render()). The right list though does not re-render, even though it gets the same props.
Why does the left list rerender, but the right list not? The reducer is immutable, the selector is too and even the length of the list changes from one to zero which should eliminate the possibility of a shallow equal.
And now for the code:
waitercallsReducer:
import { createSelector } from "reselect";
const initialState = {};
const waitercallsReducer = (state = initialState, action) => {
if (action.payload && action.payload.entities && action.payload.entities.waitercalls) {
return {
...state,
...action.payload.entities.waitercalls
};
} else {
return state;
}
};
export default waitercallsReducer;
export const getAllWaitercallsNormalizedSelector = state => state.waitercalls;
export const getAllWaitercallsSelector = createSelector(
getAllWaitercallsNormalizedSelector,
waitercalls => Object.values(waitercalls)
);
export const getAllActiveWaitercallsSelector = createSelector(
getAllWaitercallsSelector,
waitercalls => waitercalls.filter(waitercall => !waitercall.done)
);
Action creators:
import { setValues } from "../core/core";
// feature name
export const WAITERCALLS = "[Waitercalls]";
// action creators
export const setValues = (values, type) => ({
type: `SET ${type}`,
payload: values,
meta: { feature: type }
});
export const setWaitercalls = waitercalls => setValues(waitercalls, WAITERCALLS);
HomeScreen:
import React, { Component } from "react";
import { View, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-navigation";
import { connect } from "react-redux";
import { Icon } from "react-native-elements";
import PropTypes from "prop-types";
// ... I've omitted all the imports so that it's shorter
export class HomeScreen extends Component {
// ... I've omitted navigationOptions and propTypes
render() {
const {
checkins,
customChoiceItems,
menuItemPrices,
menuItems,
orders,
pickedRestaurant,
tables,
waitercalls
} = this.props;
console.log("Rendering HomeScreen");
return (
<SafeAreaView style={styles.container}>
<View style={styles.activeOrders}>
<OrdersList
checkins={checkins}
customChoiceItems={customChoiceItems}
menuItemPrices={menuItemPrices}
menuItems={menuItems}
orders={orders}
restaurantSlug={pickedRestaurant.slug}
tables={tables}
waitercalls={waitercalls}
/>
</View>
<View style={styles.tableOvewView}>
<TableOverview
checkins={checkins}
orders={orders}
tables={tables}
waitercalls={waitercalls}
/>
</View>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
checkins: getAllCheckinsSelector(state),
customChoiceItems: getAllCustomChoiceItemsNormalizedSelector(state),
menuItemPrices: getAllMenuItemPricesNormalizedSelector(state),
menuItems: getAllMenuItemsNormalizedSelector(state),
orders: getActiveOrdersSelector(state),
pickedRestaurant: getPickedRestaurantSelector(state),
tables: getAllTablesSelector(state),
waitercalls: getAllActiveWaitercallsSelector(state)
});
export default connect(mapStateToProps)(HomeScreen);
OrdersList (as you can see OrdersList also allows presses for orders, which displays the same erroneous behaviour of not having the TableOverView rerender), which is the left list with the clickable <ListItem />s.
import React, { PureComponent } from "react";
import { FlatList, Image, Text } from "react-native";
import { ListItem } from "react-native-elements";
import { connect } from "react-redux";
import PropTypes from "prop-types";
// ... omitted imports
export class OrdersList extends PureComponent {
// omitted propTypes
keyExtractor = item => item.uuid;
registerItem = item => {
// Remember the order status, in case the request fails.
const { restaurantSlug, setOrders } = this.props;
const itemStatus = item.orderStatus;
const data = {
restaurant_slug: restaurantSlug,
order_status: "registered",
order_uuid: item.uuid
};
setOrders({
entities: { orders: { [item.uuid]: { ...item, orderStatus: data.order_status } } }
});
postOrderStatusCreate(data)
.then(() => {})
.catch(err => {
// If the request fails, revert the order status change and display an alert!
alert(err);
setOrders({ entities: { orders: { [item.uuid]: { ...item, orderStatus: itemStatus } } } });
});
};
answerWaitercall = item => {
const { restaurantSlug, setWaitercalls } = this.props;
const data = {
done: true,
restaurant_slug: restaurantSlug
};
setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: true } } } });
putUpdateWaitercall(item.uuid, data)
.then(() => {})
.catch(err => {
alert(err);
setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: false } } } });
});
};
renderItem = ({ item }) => {
const { checkins, customChoiceItems, menuItemPrices, menuItems, tables } = this.props;
return item.menuItem ? (
<ListItem
title={`${item.amount}x ${menuItems[item.menuItem].name}`}
leftElement={
<Text style={styles.amount}>
{tables.find(table => table.checkins.includes(item.checkin)).tableNumber}
</Text>
}
rightTitle={`${
menuItemPrices[item.menuItemPrice].label
? menuItemPrices[item.menuItemPrice].label
: menuItemPrices[item.menuItemPrice].size
? menuItemPrices[item.menuItemPrice].size.size +
menuItemPrices[item.menuItemPrice].size.unit
: ""
}`}
subtitle={`${
item.customChoiceItems.length > 0
? item.customChoiceItems.reduce((acc, customChoiceItem, index, arr) => {
acc += customChoiceItems[customChoiceItem].name;
acc += index < arr.length - 1 || item.wish ? "\n" : "";
return acc;
}, "")
: null
}${item.wish ? "\n" + item.wish : ""}`}
onPress={() => this.registerItem(item)}
containerStyle={styles.alignTop}
bottomDivider={true}
/>
) : (
<ListItem
title={
item.waitercallType === "bill"
? SCREEN_TEXT_HOME_BILL_CALLED
: SCREEN_TEXT_HOME_SERVICE_ASKED
}
leftElement={
<Text style={styles.amount}>
{
tables.find(table =>
table.checkins.includes(
checkins.find(checkin => checkin.consumer === item.consumer).uuid
)
).tableNumber
}
</Text>
}
rightIcon={{
type: "ionicon",
name: item.waitercallType === "bill" ? "logo-euro" : "ios-help-circle-outline"
}}
onPress={() => this.answerWaitercall(item)}
bottomDivider={true}
/>
);
};
render() {
const { orders, waitercalls } = this.props;
return (
<FlatList
keyExtractor={this.keyExtractor}
data={[...orders, ...waitercalls]}
renderItem={this.renderItem}
// ... omitted ListHeader and ListEmpty properties
/>
);
}
}
export default connect(
null,
{ setOrders, setWaitercalls }
)(OrdersList);
TableOverview, which is the right <FlatList />:
import React, { Component } from "react";
import { FlatList } from "react-native";
import PropTypes from "prop-types";
// ... omitted imports
export class TableOverview extends Component {
// ... omitted propTypes
keyExtractor = item => item.uuid;
renderItem = ({ item }) => {
const { checkins, orders, waitercalls } = this.props;
if (item.invisible) return <Table table={item} />;
console.log("Rendering TableOverview");
return (
<Table
table={item}
hasActiveOrders={orders.some(order => item.userOrders.includes(order.uuid))}
billWanted={item.checkins.some(checkin =>
waitercalls.some(
waitercall =>
waitercall.waitercallType === "bill" &&
waitercall.consumer ===
checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
)
)}
serviceWanted={item.checkins.some(checkin =>
waitercalls.some(
waitercall =>
waitercall.waitercallType === "waiter" &&
waitercall.consumer ===
checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
)
)}
/>
);
};
formatData = (data, numColumns) => {
const numberOfFullRows = Math.floor(data.length / numColumns);
let numberOfElementsLastRow = data.length - numberOfFullRows * numColumns;
while (numberOfElementsLastRow !== numColumns && numberOfElementsLastRow !== 0) {
data.push({ uuid: `blank-${numberOfElementsLastRow}`, invisible: true });
numberOfElementsLastRow++;
}
return data;
};
render() {
const { tables } = this.props;
return (
<FlatList
style={styles.container}
keyExtractor={this.keyExtractor}
data={this.formatData(tables, NUM_COLUMNS)}
renderItem={this.renderItem}
numColumns={NUM_COLUMNS}
/>
);
}
}
export default TableOverview;
I found the solution!
The List was not rerendering, because the <FlatList /> only looked at the tables and not the waitercalls.
I had to add the following property to the <FlatList />:
extraData={[...checkins, ...orders, ...waitercalls]}

Resources