How to virtualize a FlatList inside FlatList (RN Web)? - react-native

How do I convince a vertical React Native FlatList to virtualize correctly inside another vertical (non-virtualizing) FlatList, in React Native Web?
So far, it seems that by default, scrolling to a certain point or responsive resize re-renderings tend to cause the virtualization to go haywire. This Snack demonstrates the problem. Be sure you're on the "Web" tab as the device builds seem to work correctly. Here's a repro through codesandbox too.
Update: Per request, here's the code inline as well. This is a full program that can paste into, say, a new expo init project (or similar) to see the strange behavior and experiment with it.
import React, { useCallback } from 'react';
import { FlatList, Text, useWindowDimensions, View } from 'react-native';
// Make 200 rows for the big list (which will draw green and red with some info).
const bigListData = Array(200).fill(0).map((element, index) => index);
function onViewableChange({ viewableItems }) {
if (viewableItems.length < 2) {
console.log(`VIEWABLE CHANGE! Only ${viewableItems.length} visible...`);
} else {
console.log(`VIEWABLE CHANGE! ${viewableItems[0].index} to ${viewableItems[viewableItems.length - 1].index}`);
}
}
function BigList() {
const { height, width } = useWindowDimensions();
const betweenRows = 10;
const itemHeight = height / 8;
const totalRowHeight = itemHeight + betweenRows;
const renderer = useCallback(({ item }) => {
const key = `i_${item}`;
return <View key={key} style={{
backgroundColor: item % 2 ? "red" : "green",
height: itemHeight,
width: '90%',
marginLeft: '5%',
marginBottom: betweenRows }}>
<Text>{key}, rh: {totalRowHeight}, offset: {totalRowHeight * item}, i {item}</Text>
</View>;
}, [itemHeight, totalRowHeight]);
const getItemLayout = useCallback((__data, index) => ({
index,
length: itemHeight,
offset: index * totalRowHeight
}), [itemHeight, totalRowHeight]);
return <FlatList
data={bigListData}
getItemLayout={getItemLayout}
key={'flatList'}
numColumns={1}
onViewableItemsChanged={onViewableChange}
renderItem={renderer}
/>;
}
function NoNestedFlatLists() {
const windowHeight = useWindowDimensions().height;
return <View style={{ height: windowHeight, width: '80%' }}><BigList /></View>;
}
function renderComponent({ item }) {
if (item.type === "widget") {
// Using height 600 here, but assume we cannot easily predict this height (due to text wrappings).
return <View key={item.type} style={{ backgroundColor: 'blue', height: 600, width: '100%', marginBottom: 15 }} />
}
return <BigList key={item.type} />;
}
function NestedFlatLists() {
const windowHeight = useWindowDimensions().height;
const components = [{ type: "widget" }, { type: "bigList" }];
return <FlatList
data={components}
key={'dynamicAppFlatList'}
numColumns={1}
renderItem={renderComponent}
style={{ height: windowHeight, width: '80%' }}
/>;
}
export default function App() {
const windowHeight = useWindowDimensions().height;
// Rendering just the following has no virtualization issues.
// The viewable change events make sense, no items suddenly disappear, no complete app meltdown...
//return <NoNestedFlatLists />;
// However:
// Any useful dynamic "rows of components" architecture melts down when virtualization comes into play.
// This sample represents such an app whose feeds have asked the app to render a "widget" followed by a
// "bigList" who could well have a few hundred items itself and thus really needs virtualization to work
// well on low-end devices. This demo leans on console logs. In snack.expo.dev, at time of writing, these
// feel hidden: Click the footer bar, either on the checkmark or an empty space, and then the "Logs" tab.
// Once you scroll down about half way in the "App", even slowly, you'll get logs like the following:
// Chrome: VIEWABLE CHANGE! 83 to 90
// Chrome: VIEWABLE CHANGE! 85 to 92
// Chrome: VIEWABLE CHANGE! Only 0 visible...
// Chrome: VIEWABLE CHANGE! 176 to 183
// Chrome: VIEWABLE CHANGE! 177 to 184
// At which time, all the UI disappears. What it thinks is viewable is quite wrong. Try to scroll around,
// but none of the mid rows are drawing. There is no easy way to repair app behavior from this state. The
// only rows which still draw correctly during the problem are the top and bottom non-virtualizing rows.
//
// As an alternate repro, you can scroll to near the middle and then resize the bottom of the window, and
// similar virtualization problems can occur. (In our real app, we can be scrolled almost anywhere out of
// the non-virtualizing rows, and make a 1px window resize to break the app. We have a more complex app
// structure, but I'm hoping a fix for this snack will still be applicable to our own symptoms...)
return <NestedFlatLists />;
}
Hopefully I am missing something trivial, as it seems clear React Native is attempting to handle nested FlatLists of the same orientation, and for the most part does great. Until you happen to have enough data items to bring virtualization into play, and even then, only fails for Web. (We've tried upgrading React Native to all the way to 0.67.2 and React Native Web to 0.17.5 - the latest releases - with no luck, and none of the Expo dropdown versions yield correct behavior in the linked Snack either.) What can I change in either sample to have correct virtualization in the nested FlatList?

Short answer is: You can't convince FlatList to virtualize this way correctly. At least currently (0.17), it's broken.
Although I was able to get some FlatList virtualization improvements into React Native Web's 0.18 preview, ultimately the measurement problems are deeper than I could afford to spend more weeks to fully fix. (If someone wants to try picking up from there - I recommend to focus on reconciling RNW's ScrollView versus RN's ScrollView and then digging into the ScrollView's measurements going absolutely haywire in the repro scenario, if replicating RN's evolution of ScrollView to RNW isn't enough.)
It ended up being much faster though to build our own virtualizing list component from scratch. Ours is specialized to our needs ATM so probably won't become open source, but who knows. But if you need to go this route... think about throttling reactions to scroll events and such to ".measure" the container view ref periodically and decide which things you need to render versus just rendering reserved empty space for... etc. There are other approaches but that seems to work.

Related

How do I create a proper text editor in React Native?

I'm creating a note taking app on React Native, and at the moment the text editor is an enhanced TextInput with some extra functionalities like copying, pasting, inserting dates, etc.
The problem is that this is very limited as I can't add line numbers, nor change styles, coloring text, etc. Performance is also a concern for big documents.
I'm experimenting with splitting the text into lines and create one text input per line, but some problems appear: I can't select text across lines, I have to handle individual keystrokes to catch line breaks, the cursor won't move between text inputs, etc.
Another problem is that I can catch soft keyboard events, but no physical keyboard events, as per the onKeyPress documentation.
I wonder whether there is a good solution for this as it seems right now that using TextInputs won't allow me to do what I need.
A good answer would be either a good library, or directions on how to do this by hand, directly using components and catching keyboard events (assuming that this is even possible).
For clarification, I don't want a rich text editor library, I want the tools to build it. I also don't want to use a webview.
This is is the best thing I have found so far. It consists on adding Text components as children to the TextInput. It dates from 2015 and does not fully solve the problem though, but it's a start.
<TextInput
ref={this.textInputRef}
style={styles.input}
multiline={true}
scrollEnabled={true}
onChangeText={this.onChangeText}
>{
lines.map((line, index) => {
return <Text>{line + '\n'}</Text>;
})
}</TextInput>
It also confirms that this is not a trivial thing to do in React Native.
Commit in the React Native GitHub repository: Added rich text input support
According to this, images can be added too (but I haven't tested it).
I will edit if I find something else.
I will update later (I have to go to work), so I will post what I have and leave comments on what I was thinking
import React, {
useState, useEffect, useRef,
} from 'react';
import {
View, StyleSheet, TextInput, Text, useWindowDimensions,
KeyboardAvoidingView, ScrollView
} from 'react-native';
const TextEditor = ({lineHeight=20}) => {
const { height, width } = useWindowDimensions()
// determine max number of TextInputs you can make
const maxLines = Math.floor(height/lineHeight);
const [ textLines, setTextLines ] = useState(Array(maxLines).fill(''));
return (
<KeyboardAvoidingView
style={styles.container}
behavior={"height"}
>
<ScrollView style={{height:height,width:'100%'}}>
{/*Make TextInputs to fill whole screen*/}
<View style={{justifyContent:'flex-end'}}>
{textLines.map((text,i)=>{
let style = [styles.textInput,{height:lineHeight}]
// if first line give it extra space to look like notebook paper
if(i ==0)
style.push({height:lineHeight*3,paddingTop:lineHeight*2})
return (
<>
<TextInput
style={style}
onChangeText={newVal=>{
textLines[i] = newVal
setTextLines(textLines)
}}
key={"textinput-"+i}
/>
</>
)
})}
</View>
</ScrollView>
</KeyboardAvoidingView>
)
}
const styles = StyleSheet.create({
container:{
flex:1,
// paddingTop:40
},
textInput:{
padding:0,
margin:0,
borderBottomColor:'lightblue',
borderBottomWidth:1,
width:'100%',
}
})
export default TextEditor
Here's what I'm thinking:
wrap this in a ScrollView and use onEndReached prop to render an additional TextInput if the last TextInput is in focused and has reached maxed character limit
store all textinput values in an array

Janky translateY on Reanimated View inside of ScrollView as user scrolls

I am using react-native-reanimated v1. I want to make a <Reanimated.View> appear as if it is fixed within the scroll view by using translateY. GIF of how of it should behave is at very end of post. I have simplified the code in the snippet below, and I have the full working code in the snack.
https://snack.expo.io/#noitidart/reanimated-scroll-view
As you scroll, you notice the position of the view is not staying fixed at the top. I attached a video taken on iOS of the snack.
If I add scrollEventThrottle={16} it fixes the issue on iOS, but on Android if you scroll even a little faster than normal you see the transform is lagging. I think there should be a way on iOS without the scrollEventThrottle property too, it doesn't make sense to me that we need this as reanimated is supposed to update every frame.
Any ideas on how to fix this?
const ReanimatedScrollView = Reanimated.createAnimatedComponent(ScrollView);
export default function App() {
const translateY = Reanimated.useValue(0);
const handleScroll = Reanimated.event([
{
nativeEvent: nativeEvent => Reanimated.block([Reanimated.set(translateY, nativeEvent.contentOffset.y)])
}])
return (
<ReanimatedScrollView onScroll={handleScroll}>
<Reanimated.View style={{ transform: [{ translateY }] }} />
</ReanimatedScrollView>
);
}
Janky on iOS
Janky on Android (with and without scrollEventThrottle={16})
Here is video of how it should be (with scrollEventThrottle={16} on iOS, but it doesn't fix up Android)

Is there a way to animate the increased size of a View when new children are added?

I’m currently using LayoutAnimation to animate a view when children are added. However, since LayoutAnimation causes everything to be animated, globally, and I can’t easily use built-in Animated library to fit my use-case, I’m wondering if react-native-reanimated is able to help.
Here's a snack of my current solution:
https://snack.expo.io/#insats/height-adapation
This is what the result of that looks like:
Is there a way to achieve the same thing without using LayoutAnimation? I've looked through all exampled in react-native-reanimated, and I've read through the docs but I'm still not sure if this is possible to do or how I should get started. I've thought about using Animated to move the item-wrapper out of the viewable area and "scroll" it upwards (using transform translateY) when items are added, but that would require fixed height, which I don't have.
I have 2 approaches that I can suggest out of my mind:
You can configure your LayoutAnimation only when your desired state changed. If you use hooks it would be too easy:
const [state,setState] = useState([]);
useEffect(()=>{
/*rest code*/
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
},[state])
Or if you use class component you can catch your desired state change in componentDidUpdate:
componentDidUpdate(prevProps,prevState){
if(prevState.items!==state.items){
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
}
}
You can use onLayout function of view:
addItem = () => {
this.setState({
items: [...this.state.items, {title:'An item',isNew:true}]
})
};
renderItems = () => {
return this.state.items.map((item, index) => {
let opacity = new Animated.Value(0);
return (
<Animated.View onLayout={({nativeEvent})=>{
if(this.state.item.isNew){
// here you got the height from nativeEvent.layout.height
// Then you have to store the height animate height and opacity to its precise value
// PS I used opacity:0 to calculate the height
}
}} key={index} style={[styles.item,{opacity}>
<Text>{item.title}</Text>
</View>
)
});
};
When it comes to react-native-reanimated I regard it as more faster version of react-native's Animated library. So either way you will have to calculate the height!

Render Three.js Scene in react-360?

I have a BoxGeometry added to a three.js scene. I have also added the scene in ReactInstance. The scene however doesn't seem to be rendered? I have tried this but doesn't work. just wanted to know in what react component the scene would be rendered?
Cube.js:
import {Module} from 'react-360-web';
import * as THREE from 'three';
export default class Cube extends Module {
scene: THREE.scene;
constructor(scene) {
super('Cube123');
this.scene = scene;
}
add() {
const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshBasicMaterial({ color: Math.random() * 0xffffff });
const mesh = new THREE.Mesh(geometry, material);
mesh.position.z = -4;
this.scene.add(mesh);
}
}
client.js:
import {ReactInstance, Location, Surface} from 'react-360-web';
import Cube from './Cube';
import * as THREE from 'three';
function init(bundle, parent, options = {}) {
const scene = new THREE.Scene();
const Cube123 = new Cube(scene);
const r360 = new ReactInstance(bundle, parent, {
fullScreen: true,
nativeModules: [ Cube123 ],
scene: scene,
...options,
});
r360.scene = scene;
r360.renderToLocation(
r360.createRoot('CubeModule123'),
new Location([0, -2, -10]),
);
r360.compositor.setBackground('./static_assets/360_world.jpg');
}
window.React360 = {init};
CubeModule.js:
import * as React from 'react';
import {Animated, View, asset, NativeModules} from 'react-360';
import Entity from 'Entity';
import AmbientLight from 'AmbientLight';
import PointLight from 'PointLight';
const Cube123 = NativeModules.Cube123;
export default class CubeModule extends React.Component{
constructor() {
super();
Cube123.add();
}
render() {
return (
<Animated.View
style={{
height: 100,
width: 200,
transform: [{translate: [0, 0, -3]}],
backgroundColor: 'rgba(255, 255, 255, 0.4)',
layoutOrigin: [0.5, 0, 0],
alignItems: 'center',
}}
>
</Animated.View>
);
}
}
I know this doesn't answer the question exactly but take a look at this web page from react 360:
React 360 what is it?
Specifically, take a look at this:
How is React 360 Different from A-Frame?
A-Frame is a framework for building VR worlds using declarative HTML-like components. It has a rich collection of available components from a vibrant community, and is great for creating intricate 3D scenes that can be viewed in VR. We believe that React 360 serves a different use case optimized around applications that rely on user interfaces, or are event-driven in nature. Look through our examples to see the types of things you can easily build with React 360.
Trying to figure out which framework is right for you? Here's a quick test. If your application is driven by user interaction, and has many 2D or 3D UI elements, React 360 will provide the tools you need. If your application consists of many 3D objects, or relies on complex effects like shaders and post-processing, you'll get better support from A-Frame. Either way, you'll be building great immersive experiences that are VR-ready!
How is React 360 Different from Three.js?
Three.js is a library for 3D rendering in the web browser. It's a much lower-level tool than React 360, and requires control of raw 3D meshes and textures. React 360 is designed to hide much of this from you unless it's needed, so that you can focus on the behavior and appearance of your application.
Currently, React 360 relies on Three.js for some of its rendering work. However we are opening up the relevant APIs so that React 360 developers may soon be able to use the 3D rendering library of their choice, including raw WebGL calls.
I know it's probably not the answer you wanted but honestly as someone who has dabbled a lot with aframe and react 360, if you want to use cubes, spherse, shapes, etc. You should go with Aframe. This question has been asked on the github issues on the react360 page and the consensus was the same. Theoretically it is possible, but you'll have to bend over backwards just to make it work.

Screen width in React Native

How do I get screen width in React native?
I need it because I use some absolute components that overlap and their position on screen changes with different devices.
In React-Native we have an Option called Dimensions
Include Dimensions at the top var where you have include the Image,and Text and other components.
Then in your Stylesheets you can use as below,
ex: {
width: Dimensions.get('window').width,
height: Dimensions.get('window').height
}
In this way you can get the device window and height.
Simply declare this code to get device width
let deviceWidth = Dimensions.get('window').width
Maybe it's obviously but, Dimensions is an react-native import
import { Dimensions } from 'react-native'
Dimensions will not work without that
April 10th 2020 Answer:
The suggested answer using Dimensions is now discouraged. See: https://reactnative.dev/docs/dimensions
The recommended approach is using the useWindowDimensions hook in React; https://reactnative.dev/docs/usewindowdimensions which uses a hook based API and will also update your value when the screen value changes (on screen rotation for example):
import {useWindowDimensions} from 'react-native';
const windowWidth = useWindowDimensions().width;
const windowHeight = useWindowDimensions().height;
Note: useWindowDimensions is only available from React Native 0.61.0: https://reactnative.dev/blog/2019/09/18/version-0.61
If you have a Style component that you can require from your Component, then you could have something like this at the top of the file:
const Dimensions = require('Dimensions');
const window = Dimensions.get('window');
And then you could provide fulscreen: {width: window.width, height: window.height}, in your Style component. Hope this helps
React Native Dimensions is only a partial answer to this question, I came here looking for the actual pixel size of the screen, and the Dimensions actually gives you density independent layout size.
You can use React Native Pixel Ratio to get the actual pixel size of the screen.
You need the import statement for both Dimenions and PixelRatio
import { Dimensions, PixelRatio } from 'react-native';
You can use object destructuring to create width and height globals or put it in stylesheets as others suggest, but beware this won't update on device reorientation.
const { width, height } = Dimensions.get('window');
From React Native Dimension Docs:
Note: Although dimensions are available immediately, they may change (e.g due to >device rotation) so any rendering logic or styles that depend on these constants >should try to call this function on every render, rather than caching the value >(for example, using inline styles rather than setting a value in a StyleSheet).
PixelRatio Docs link for those who are curious, but not much more there.
To actually get the screen size use:
PixelRatio.getPixelSizeForLayoutSize(width);
or if you don't want width and height to be globals you can use it anywhere like this
PixelRatio.getPixelSizeForLayoutSize(Dimensions.get('window').width);
React Native comes with "Dimensions" api which we need to import from 'react-native'
import { Dimensions } from 'react-native';
Then,
<Image source={pic} style={{width: Dimensions.get('window').width, height: Dimensions.get('window').height}}></Image>
Only two simple steps.
import { Dimensions } from 'react-native' at top of your file.
const { height } = Dimensions.get('window');
now the window screen height is stored in the height variable.
Just discovered react-native-responsive-screen repo here. Found it very handy.
react-native-responsive-screen is a small library that provides 2 simple methods so that React Native developers can code their UI elements fully responsive. No media queries needed.
It also provides an optional third method for screen orienation detection and automatic rerendering according to new dimensions.
First get Dimensions from react-native
import { Dimensions } from 'react-native';
then
const windowWidth = Dimensions.get('window').width;
const windowHeight = Dimensions.get('window').height;
in windowWidth you will find the width of the screen while in windowHeight you will find the height of the screen.
The latest method from 2020 is to use useWindowDimensions
the way i implemented it-
make a global.js file and set window width and height as global variables
const WindowDimensions= (()=>{
global.windowWidth = useWindowDimensions().width;
global.windowHeight = useWindowDimensions().height;
return (<></>);
})
in App.js file,import window dimensions and add it to return block
use width and height everywhere as global.windowHeight and global.windowWidth
using global variables is not a good design pattern. but this thing works
I think using react-native-responsive-dimensions might help you a little better on your case.
You can still get:
device-width by using and responsiveScreenWidth(100)
and
device-height by using and responsiveScreenHeight(100)
You also can more easily arrange the locations of your absolute components by setting margins and position values with proportioning it over 100% of the width and height
You can achieve this by creating a component and using it by importing it into the file you need.
import {Dimensions, PixelRatio} from "react-native";
const {width, height} = Dimensions.get("window");
const wp = (number) => {
let givenWidth = typeof number === "number" ? number : parseFloat(number);
return PixelRatio.roundToNearestPixel((width * givenWidth) / 100);
};
const hp = (number) => {
let givenHeight = typeof number === "number" ? number : parseFloat(number);
return PixelRatio.roundToNearestPixel((height * givenHeight) / 100);
};
export {wp, hp};
Now, you should use it.
import { hp, wp } from "../<YOUR PATH>";
buttonContainer: {
marginTop: hp("2%"),
height: hp("7%"),
justifyContent: "center",
alignSelf: "center",
width: wp("70%"),
backgroundColor: "#1C6AFD",
borderRadius: 5,
},
buttonText: {
fontSize: wp("3.5%"),
color: "#dddddd",
textAlign: "center",
fontFamily: "Spartan-Bold",
}
That way, you will make your design responsive. I suggest using simple pixels in the styling of circle things like avatar images, etc.
In other cases, the above code wraps components according to the density pixels of the screen.
If you have any better solution, please comment.
First, you must import Dimensions from 'react-native'
import { View, StyleSheet, Dimensions } from "react-native";
after that, you can save width and height in variables:
const windowsWidth = Dimensions.get('window').width
const windowsHeight = Dimensions.get('window').height
them you could use both as you need, i.e. in styles:
flexDirection: windowsWidth<400 ? 'column' : 'row',
Remember this, your object styles is outside your component, so the cariable declaration must be outside your component too. But if you need it inside your component, no problem, can use it:
<Text> Width: { windowsWidth }</Text>
<Text> Height: { windowsHeight }</Text>
you can get device width and height in React Native, by the following code:
const windowWidth = Dimensions.get('window').width;
const windowHeight = Dimensions.get('window').height;
docs: https://reactnative.dev/docs/dimensions
import {useWindowDimensions, Dimensions} from 'react-native'
let width1= useWindowDimensions().width // Hook can be called only inside functional component, tthis is dynamic
let width2=Dimensions.get("screen").width