I'm trying to recreate html table functionality in React Native and I just cannot figure this out.
I have 1 column for item name (short), and another for item description (potentially very long).
I want the first column to take up only as much space as it needs and the second column to flex and then text-wrap when it runs out of room. That part's easy, but then the items in the first column all have different widths. And if I make the columns first instead in order to solve that problem, then getting the corresponding items in the first column to flex vertically to stay aligned is tripping me up. HTML tables do this effortlessly. Why is it so hard in Native?
Is there really no way to do a truly flexible table in this language?
I've tried different variations on flex, but I don't want it to be a fixed width or ratio for either column, since I want to leave the option for font sizes later, which would break it.
react-native-paper fails because DataTable.Cell doesn't allow for multiline, and adding in the functionality messes with the alignment, bringing me right back to where I started.
EDIT: In html, I would do it like this:
<html>
<head>
<style>
td:first-child {white-space: nowrap; }
</style>
</head>
<body>
<table>
<tr>
<td>
I am some data!
</td>
<td>
Two households, both alike in dignity, in fair Verona, where we lay our scene, from ancient grudge break to new mutiny, where civil blood makes civil hands unclean.
</td>
</tr>
<tr>
<td>
I'm data too!
</td>
<td>
From forth the fatal loins of these two foes, a pair of star-cross'd lovers take their life; whose misadventured piteous overthrows do with their death bury their parents' strife.
</td>
</tr>
<tr>
<td>
I am also some data!
</td>
<td>
The fearful passage of their death-mark'd love, and the continuance of their parents' rage, which, but their children's end, nought could remove, is now the two hours' traffic of our stage; the which if you with patient ears attend, what here shall miss, our toil shall strive to mend.
</td>
</tr>
</table>
</body>
</html>
resulting in:
You could create a component called Table which represents the table itself and one component called TableRow which represents one row of the table.
import React from 'react';
import { View } from 'react-native';
import TableRow from './TableRow';
const Table = () => (
<View style={{ flexDirection: 'column' }}>
<TableRow itemName="Item 1" itemDescription="Description for item 1" />
<TableRow itemName="Item 2" itemDescription="Description for item 2" />
<TableRow itemName="Item 3" itemDescription="Description for item 3" />
</View>
);
export default Table;
and
import React from 'react';
import { View, Text } from 'react-native';
const TableRow = ({ itemName, itemDescription }) => (
<View style={{ flexDirection: 'row' }}>
<View style={{ width: 'auto', alignItems: 'flex-start' }}>
<Text>{itemName}</Text>
</View>
<View style={{ flex: 3, alignItems: 'flex-start' }}>
<Text>{itemDescription}</Text>
</View>
</View>
);
export default TableRow;
in my opinion using flex: 1 instead of width: 'auto' looks better but of course i don't know what your prerequisites are.
To fill the Table with data you need pass the table component an array of items. To do this modify the Table component
import React from 'react';
import { View } from 'react-native';
import TableRow from './TableRow';
const Table = ({ items }) => (
<View style={{ flexDirection: 'column' }}>
{items.map(item => (
<TableRow
key={item.name}
itemName={item.name}
itemDescription={item.description}
/>
))}
</View>
);
export default Table;
Now you can do the following:
import React from 'react';
import Table from './Table';
const items = [
{ name: 'Item 1', description: 'Description for item 1' },
{ name: 'Item 2', description: 'Description for item 2' },
{ name: 'Item 3', description: 'Description for item 3' },
];
const App = () => (
<Table items={items} />
);
export default App;
This will fill your table with data.
Okay, I think I got pretty much the functionality I wanted. There's probably a way more extensible way to do this, and this is probably wildly inefficient, but it seems to work.
The gist is that I can use a state variable to store the desired width of each cell, and then in onLayout, I can call setColWidth to update that variable whenever the layout changes. Then, I can just use style to set the minimum width to the width of the biggest cell.
Then there's an array that determines whether a given column gets shrunk to allow room for others.
Finally, I can call alignItems in the parent View to shrink-wrap the tables to the minimum size that fits the data
import React, {useState} from 'react';
import {LayoutChangeEvent, useWindowDimensions, View} from 'react-native';
const Table = ({
data,
rowStyle = undefined,
priviledge = new Array(data.length).fill(false),
}: {
data: any[];
rowStyle: Object | undefined;
priviledge: boolean[];
}) => {
// Initialize list of widths
const [colWidth, setColWidth] = useState<number[][]>(
data.map(row => new Array(Object.keys(row).length).fill(0)),
);
// Get widows size
const maxSize = useWindowDimensions();
if (!colWidth || !maxSize) {
return <></>;
}
// Fix issues of going off screen
const onLayout = (event: LayoutChangeEvent, row: number, col: number) => {
// Get current width
var {width, __} = event.nativeEvent.layout;
// Find total row width
const sum =
colWidth[row].reduce((partialSum, a) => partialSum + a, 0) -
colWidth[row][col] +
width;
// Shrink unpriviledged components
if (!priviledge[col] && sum > maxSize.width) {
width = width - (sum - maxSize.width);
if (width < 0) {
width = 0;
}
}
// Store width in colWidth array
colWidth[row][col] = width;
setColWidth([...colWidth]);
};
return (
<View>
{/* Map along rows */}
{data.map((item, rowIndex) => (
<View
key={rowIndex}
style={{
flexDirection: 'row',
maxWidth: maxSize.width,
}}>
{/* Map along columns */}
{Object.keys(item).map((key, colIndex) => (
<View
key={key}
onLayout={event => {
onLayout(event, rowIndex, colIndex);
}}
style={{
minWidth: Math.max(...colWidth.map(row => row[colIndex])),
flexShrink: 1,
...rowStyle,
}}>
{item[key]}
</View>
))}
</View>
))}
</View>
);
};
export default Table;
Here's how it looks with some example data from my project (the yellow boxes are the tables using this code):
Android emulator showing the working code
Right now, the only problem I can see is that it doesn't update when rotating from landscape to portrait (but portrait to landscape works fine???).
Related
Scrolling a SectionList results in unreasonably low JS frame rate dips (~30) for simple cell renders (a single text label) and gets much worse if cell contents are any more complex (several text labels in each cell will result in single digit JS frame rate dips).
The user facing-problem manifested by the low JS frame rate is very slow response times when a user taps anything after scrolling (it can be ~5 seconds delay).
Here's an example repo which creates both a FlatList and a SectionList each with 10,000 items on a single screen (split laterally across the middle of the screen). It's a managed Expo app (for ease of reproduction) and it's written in Typescript (because I'm used to that). The readme describes setup steps if you need them. Run the app on a physical device and turn on React Native's performance monitor to view the JS frame rate. Then scroll each of the lists as fast as you can to view the differing effects on the JS frame rate while scrolling a FlatList vs a SectionList.
Here's the entire source (from that repo's App.tsx file; 61 lines) if you prefer to bootstrap it yourself, or eyeball it here without going to GitHub:
import React from "react"
import { FlatList, SectionList, Text, View } from "react-native"
const listSize = 10_000
const listItems: string[] = Array.from(
Array(listSize).keys(),
).map((key: number) => key.toString())
const alwaysMemoize = () => true
const ListItem = React.memo(
({ title }: { title: string }) => (
<View style={{ height: 80 }}>
<Text style={{ width: "100%", textAlign: "center" }}>{title}</Text>
</View>
),
alwaysMemoize,
)
const flatListColor = "green"
const flatListItems = listItems
const sectionListColor = "blue"
const sectionListItems = listItems.map(listItem => ({
title: listItem,
data: [listItem],
}))
const approximateStatusBarHeight = 40
const App = () => {
const renderItem = React.useCallback(
({ item }: { item: string }) => <ListItem title={item} />,
[],
)
const renderSectionHeader = React.useCallback(
({ section: { title } }: { section: { title: string } }) => (
<ListItem title={title + " section header"} />
),
[],
)
return (
<View style={{ flex: 1, paddingTop: approximateStatusBarHeight }}>
<FlatList
style={{ flex: 1, backgroundColor: flatListColor }}
data={flatListItems}
renderItem={renderItem}
keyExtractor={item => item}
/>
<SectionList
style={{ flex: 1, backgroundColor: sectionListColor }}
sections={sectionListItems}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={item => item}
/>
</View>
)
}
export default App
Have I missed some performance optimization/am I doing something wrong? Or is this React Native's expected performance?
Another question as a side note: If I don't set the height of each of the cells to something reasonably tall (eg 80 in the example above) and just let the height adjust to the height of the contained text (14pt I believe), the JS frame rate dip for both types of lists becomes quite bad; 20 JS frames or less.
I'll also note here that the performance tests shown in the screen shots were done on an iPhone 13 mini.
I have a React-Native application where I am using FlatList to display a list of items obtained from the server. The list has 2 columns and I need my list items to be the same height. I put a border around the code rendering my list items but the list items are not the same height. I have tried using flexbox settings to make the view fill the container, but everything I try makes no difference.
I have created a simplified version of my app to illustrate the issue:
See that the red bordered areas are NOT the same height. I need to get these to be the same height.
The grey border is added in the view wrapping the component responsible for a list item and the red border is the root view of the component responsible for a list item. See the code below for clarity.
I can not use the grey border in my application because my application shows empty boxes whilst the component responsible for a list item is getting additional information from the server before it renders itself
Furthermore I can not used fixed sizes for heights.
Application Project structure and code
My code is split up in a manner where the files ending in "container.js" get the data from the server and pass it to its matching rendering component. For example, "MainListContainer" would be getting the list from the server and then pass the list data to "MainList", and "ListItemContainer" would get additional information about the single list item from the server and pass it to "ListItem" to render the actual item. I have kept this model in my simplified application so its as close to my real application as possible.
index.js
import {AppRegistry} from 'react-native';
import MainListContainer from './app/components/MainListContainer';
import {name as appName} from './app.json';
AppRegistry.registerComponent(appName, () => MainListContainer);
MainListContainer.js
import React from 'react';
import MainList from './MainList';
const data = [
{id: '1', title: 'Item 1', subtitle: 'A', description: 'This is the first item.'},
{id: '2', title: 'Item 2', subtitle: 'B', description: 'The Big Brown Fox Jumped over the lazy dogs. The Big Brown Fox Jumped over the lazy dogs.',},
];
const MainListContainer = () => {
return ( <MainList items={data} /> );
};
export default MainListContainer;
MainList.js
import React from 'react';
import {StyleSheet, FlatList, View} from 'react-native';
import ListItemContainer from './ListItemContainer';
export default class MainList extends React.Component {
constructor(props) {
super(props);
this.state = { numColumns: 2};
this.renderItem = this.renderItem.bind(this);
}
renderItem({item, index}) {
return (
<View style={styles.flatListItemContainer}> <!-- THIS IS WHERE THE GREY BORDER IS ADDED -->
<ListItemContainer key={index} item={item} />
</View>
);
}
render() {
const {items} = this.props;
const {numColumns} = this.state;
return (
<View>
<FlatList
data={items}
renderItem={this.renderItem}
numColumns={numColumns}
key={numColumns}
keyExtractor={(item) => item.id}
/>
</View>
);
}
};
const styles = StyleSheet.create({
flatListItemContainer: {
flex: 1,
margin: 10,
borderColor: '#ccc',
borderWidth: 1,
},
});
ListItemContainer.js
import React from 'react';
import ListItem from './ListItem';
const ListItemContainer = (props) => {
const { item } = props;
return (
<ListItem item={item} />
);
};
export default ListItemContainer;
ListItem.js
import React from 'react';
import {TouchableHighlight, View, StyleSheet, Image, Text} from 'react-native';
const ListItem = (props) => {
const { item } = props;
return (
<TouchableHighlight
underlayColor="white"
>
<View style={styles.containerView}> <!-- THIS IS WHERE THE RED BORDER IS ADDED -->
<View style={styles.top_row}>
<Image style={styles.image} source={require('../images/placeholder.png')} />
<View style={styles.title_texts}>
<Text style={{fontWeight:'bold'}}>{item.title}</Text>
<Text style={{color: 'rgb(115, 115, 115)'}}>{item.subtitle}</Text>
</View>
</View>
<Text>{item.description}</Text>
</View>
</TouchableHighlight>
);
};
export default ListItem;
const styles = StyleSheet.create({
containerView: {
padding: 14,
borderColor: 'red',
borderWidth: 1,
},
top_row: {
flex: 1,
flexDirection: 'row',
marginBottom: 10,
},
title_texts: {
flex: 1,
flexDirection: 'column',
},
image: {
alignSelf: 'flex-end',
resizeMode: 'cover',
height: 40,
width: 40,
marginRight: 20
},
});
What I have tried
ListItem.js : move the style onto the "TouchableHighlight" view
ListItem.js : add a view wrapping "TouchableHighlight" view and adding style there
ListItem.js : added "alignItems:'stretch' on the "TouchableHighlight, added it to the "containerView" style, tried it on the description field too
same as "alignItems" but used "alignedSelf" instead
same as "alignItems" but used "alignedContent" instead
tried using "flexGrow" on different views (container, description)
You can measure the height of every element in the list and when you determine the maximum height, you can use that height for every element in the list.
const Parent = ({ ...props }) => {
const [maxHeight, setMaxHeight] = useState<number>(0);
const computeMaxHeight = (h: number) => {
if (h > maxHeight) setMaxHeight(h);
}
return (
<FlatList
data={props.data}
renderItem={({ item }) => (
<RenderItem
item={item}
computeHeight={(h) => computeMaxHeight(h)}
height={maxHeight}
/>
)}
....
/>
)
}
The Items:
const RenderItem = ({...props }) => {
return (
<View
style={{ height: props.height }}
onLayout={(event) => props.computeHeight(event.nativeEvent.layout.height)}
>
<Stuffs />
</View>
)
}
This is a very non-performant way of achieving this. I would avoid this if I have a long list or any list of more than a few items. You however can put certain checks in place to limit rerendering etc. Or alternatively if it is only text that will affect the height, then you can only measure the height of the element with the most text and use that element's height for the rest.
Instead of set fixed width height, you can use flex box to achieve it. I just solved the issue by removing alignSelf at the FlatList and add alignItems center on it.
Wrap the flatList in flex box with align item center, you can add the code in your MainList.js file, the first <View>, i.e:
render() {
const {items} = this.props;
const {numColumns} = this.state;
return (
<View style={{flex: 1, alignItems: 'center'>
<FlatList
data={items}
renderItem={this.renderItem}
numColumns={numColumns}
key={numColumns}
keyExtractor={(item) => item.id}
/>
</View>
);
If still not reflected, you may try to add flex:1, alignItems center in FlatList style props.
You are missing a very basic concept of giving fixed height to the flatlist items, in your ListItem.js, try to set height:200 in containerView. Let me know if that works for you
I want to display 30 pages of text on a screen. I've tried ScrollView and FlatList but I get a white screen. Only when I try with ScrollView to display only 2 pages, works fine.
I do not want to use a WebView, because I would like to have all data in the app (no internet connection needed).
Here is what I've already tried:
With FlatList:
I have a text.js as a model, which I use to create a Text Object in an array, which I then use as data for the FlatList. For the renderItem function (of FlatList) I use a TextItem to display the text.
text.js
function Text(info) {
this.id = info.id;
this.text = info.text;
}
export default Text;
LongTextModule.js
import Text from '../../models/text';
export const LONGTEXT = [
new Text({
id:'text_1',
text:`.....longtext....`
})
]
TextItem.js
const TextItem = (props) => {
return (
<View style={styles.screen} >
<Text style={styles.textStyle}>{props.longText}</Text>
</View >
);
};
const styles = StyleSheet.create({
screen: {
flex: 1,
},
textStyle: {
justifyContent: 'flex-start',
alignItems: 'flex-start',
fontFamily: 'GFSNeohellenic-Regular',
fontSize: 20,
padding: 10,
}
});
TextDetailScreen.js
const TextDetailScreen = (props) => {
const renderText = data => {
return <TextItem longText={data.item.text} />
}
return <FlatList
data={LONGTEXT}
keyExtractor={(item, index) => item.id}
renderItem={renderText}
/>
};
I think it's needless to show the code with ScrollView, since ScrollView is only for a small list.
I even tried to render the longText like this in the screen.
Without the ScrollView I get the first portion, but with ScrollView a white screen.
const TextDetailScreen = (props) => {
return (
<ScrollView>
<Text> ...longText...</Text>
</ScrollView>
);
};
I'm sure there is a way to display a lot of pages of text on a screen?
But how?
Thank you :)
It seems not to be an unknown Issue, I've also read from time to time about this issue.
But not to use Webview, because you wan't to have all Data in your app - don't have to be an Argument against Webview. With WebView, you also can display Data from your App-Storage.
Example:
<WebView style={styles.myStyle} source={{html: `<p style="font-size:48px">${longtext}</p>`}} />
I am seeking a way to scroll a viewport over a table like this, except that every cell is exactly the same size:
I am currently using FlatList's numColumns parameter to make a table and scroll the viewport over that table.
Here is a Snack example - RegularGridExample:
import React from 'react';
import { FlatList, Text, View } from 'react-native';
const numRows = 10,
numColumns = 10,
width = 100,
height = 100,
cells = [...Array(numRows * numColumns)].map((_, cellIndex) => {
const rowIndex = Math.floor(cellIndex / numRows),
colIndex = cellIndex % numColumns;
return {
key: `${colIndex},${rowIndex}`,
rowIndex,
colIndex,
styles: {
width,
height,
backgroundColor: 'green',
borderColor: 'black',
borderWidth: 1,
},
};
});
export default class RegularGridExample extends React.Component {
render() {
return (
<FlatList
data={cells}
renderItem={this.renderItem}
numColumns={numColumns}
horizontal={false}
columnWrapperStyle={{
borderColor: 'black',
width: numColumns * width,
}}
/>
);
}
renderItem = ({ item: { styles, rowIndex, colIndex } }) => {
return (
<View style={styles}>
<Text>r{rowIndex}</Text>
<Text>c{colIndex}</Text>
</View>
);
};
}
This example will correctly scroll to reveal the rows below the viewport, but it will not scroll to reveal the columns beyond the viewport. How can I enable scrolling the viewport to reveal a FlatList's columns?
Update 1
I do not think this can be easily solved with nested FlatLists, which is the first thing I tried before using the numColumns approach above. The use case here is shifting the viewport over a grid that's larger than the viewport, not just scrolling one row within the viewport.
Update 2
I'm seeking a virtualized solution. While the wireframe above uses text, the use case I'm actually interested in is browsing a tile server navigating over portions of a large 50MB+ image. It will be too slow to load all of them into a scroll view.
Unrelated Stack Overflow Posts
React Native ScrollView/FlatList not scrolling - this is about adding flex to the viewport to enable scrolling along the major axis of the FlatList, which is already working in the example above. My concern is scrolling along the crossAxis.
React native flatlist not scrolling - it is unclear what the expected and actual behavior is here
How can I sync two flatList scroll position in react native - here, the poster is seeking to simulate masonry layout; I'm not doing anything so fancy
This can't be done using the FlatList method, since numColumns is used which explicitly sets horizontal={false}, hence disabling the scrolling horizontal direction.
Here's a workaround by using nested ScrollViews
export default class RegularGridExample extends React.Component {
render() {
const generatedArray = [1,2,3,4,5,6]
return (
<ScrollView horizontal>
<ScrollView >
{generatedArray.map((data, index) => {
return <View style={{flexDirection: 'row'}} >
{generatedArray.map((data, index) => {
return <View style={{height: 100, width: 100, backgroundColor: 'red', borderWidth: 1, borderColor: 'black'}} />
})}
</View>
})}
</ScrollView>
</ScrollView>
);
}
}
I am trying to make a FlatList with items that can expand and collapse onPress
However, when I add a new item from another screen then go back to SearchListScreen, it will only display 2 items, but the FlatList does render the correct number of rows.
example:
Before adding new item
After adding new item
The same thing happens when I remove an item or expand a item.
Here's my code:
SearchList.js
import React, { Component } from 'react'
import { Text, View, FlatList, StyleSheet } from 'react-native'
import SearchCard from './SearchCard'
export default class SearchList extends Component {
wrapperStyle (index) {
return index > 0 ? styles.listItemWrapper : [styles.listItemWrapper, styles.wrapperFirst]
}
_renderItem = ({item, index}) => (
<View style={this.wrapperStyle(index)}>
<SearchCard
search={item}
id={item.id}
filterAttributes={this.props.filterAttributes}
onSearch={this.props.onSearch}
onFavorite={this.props.onFavorite}
favorites={this.props.favorites}
/>
</View>
)
render () {
const { searches, filterAttributes, onSearch, onFavorite, favorites } = this.props
return (
<FlatList
data={searches}
extraData={{ filterAttributes: filterAttributes, onSearch: onSearch, onFavorite: onFavorite, favorites: favorites, searches: searches }}
keyExtractor={item => item.id}
renderItem={this._renderItem}
enableEmptySections
style={{backgroundColor: 'red'}}
/>
)
}
}
const styles = StyleSheet.create({
wrapperFirst: {
marginTop: 20
},
listItemWrapper: {
marginLeft: 20,
marginRight: 20,
marginBottom: 20
}
})
After hours of struggling, I find that adding a height to the item solved the problem.
It could be an issue related to the styles of StyleSheet applied to either the row or the FlatList itself. In my case I applied a wrong style property to the FlatList which in return did not display the list.