I am facing what seems to be a long-lasting issue in react native.
I am using Expo SDK35 with RN version 0.59. I have not updated to Expo SDK36 / RN 0.60 yet, due to large code base, but I could update if that makes up for a solution to my issue.
I have an Animated.View component that has a FlatList child, and I am unable to use the static methods (scrollToIndex() in particular) that should be available on the FlatList reference. See the next example code:
class Example extends React.Component{
constructor(props){
super(props);
this.myRef = null;
}
componentDidUpdate = () => {
/*
somewhere in code outside this class, a re-render triggers
and passes new props to this class.
I do have props change detection, and some more other code,
but I have removed it in order to minimize the code example here
*/
// This call throws:
// TypeError: undefined is not a function (near '...this._scrollRef.scrollTo...')
this.myRef.scrollToIndex({
animated: true,
index: 1,
viewOffset: 0,
viewPosition: 0.5
});
// Other suggested solution from SO
// This also throws:
// TypeError: _this.myRef.getNode is not a function. (In '_this.myRef.getNode()', '_this.myRef.getNode' is undefined)
this.myRef.getNode().scrollToIndex({
animated: true,
index: 1,
viewOffset: 0,
viewPosition: 0.5
});
}
render = () => <Animated.View style={{ /* ... some animated props */ }}>
<FlatList ref={(flatListRef) => { this.myRef = flatListRef; }}
// more FlatList related props
/>
</Animated.View>
}
I have tried to use Animated.FlatList instead, still throws the same errors as in the code example above.
I have also tried to use react native's findNodeHandle() utility function on the received flatListRef parameter, but it returns null.
I have found the same issue posted multiple times in the past here on Stack Overflow, most with no answer, or which do not work for me. These posts are also a bit old (a year or so), which is why I am posting again for the same issue.
Did anyone manage to find a solution/workaround for this issue?
EDIT: Possible workaround
As I was playing with code, I tried to use a ScrollView component instead of FlatList - and the scrollTo method works!
The changes were only on the FlatList - ScrollView specific props (so, for a ScrolLView it would be childs instead of data={[...]} and renderItem={()=>{ ... }}, ect.), and the scrollToIndex method in componentDidMount which was replaced by scrollTo.
The render method of the class, with a ScrollView, now looks like this:
render = () => <Animated.View style={{ /* ... some animated props */ }}>
<ScrollView ref={(flatListRef) => { this.myRef = flatListRef; }}>
{/*
this.renderItem is almost the same as the
renderItem method used on the FlatList
*/}
{ this.state.dataArray.map(this.renderItem) }
</ScrollView>
</Animated.View>
Please note that ScrollView does not have a scrollToIndex() method, so you'll have to cope with manually keeping track of child positions, and maybe, implement a scrollToIndex method of your own.
I am not making this the answer to my question, because the underlying issue remains. But as a workaround, maybe you can go with it and call it a day...
TL;DR;
this.myRef = React.createRef();
this.myRef.current.doSomething(); // note the use of 'current'
Long version:
While the idea behind what I was trying was correct, the error in my original post seems to be quite stupid. In my defense, the docs were not clear (probably...). Anyway...
React.createRef returns an object with a few fields on it, all of them useless for the developer (used by React in the back) - except one: current.
This prop holds the current reference to the underlying component that the ref is attached to. The main ref object is not usable for the purpose I meant to in my original question above.
Instead, this is how I should've used the ref correctly:
this.myRef.current.scrollToIndex(...)
Hold up, don't crash
Both the main myRef object, and the current field will be null if the component has not yet mounted, has unmounted at any point later, or if the ref cannot be attached to it for some reason. As you may know (or found out later), null.something will throw an error. So, to avoid it:
if ((this.myRef !== null) && (this.myRef.current !== null)){
this.myRef.current.scrollToIndex(...);
}
Extra insurance
If you try to call an undefined value as a function on a field on the ref, your code will crash. This can happend if you mistakenly reuse the same ref on multiple components, or if the component you attached it to does not have that method (i.e. View does not have a scrollTo method). To fix this you have two solutions:
// I find this to be the most elegant solution
if ((this.myRef !== null) && (this.myRef.current !== null)) {
if (typeof this.myRef.current.scrollToIndex === "function") {
this.myRef.current.scrollToIndex(...);
}
}
or
if ((this.myRef !== null) && (this.myRef.current !== null)) {
if (typeof this.myRef.current.scrollToIndex === "function") {
try {
this.myRef.current.scrollToIndex(...);
} catch (error) {
console.warn("Something went wrong", error);
}
}
}
I hope this to be useful for anyone else learning to use refs in React. Cheers :)
With Animated.ScrollView:
Create a ref to your FlatList (the old way only works):
<ScrollView ref={ (ref) => (this.MyRef=ref) } />
Access scrollToIndex using this.myRef.getNode().scrollToIndex
Animated.FlatList is currently not working unfortunately...
With FlatList:
Create a ref to your FlatList by:
<FlatList ref={ this.flatListRef } />
constructor(props) {
super(props);
this.flatListRef = React.createRef();
}
Access scrollToIndex using this.flatListRef.current.scrollToIndex
Also make sure to wrap your code inside an if statement like:
if (this.myRef.getNode()) { this.flatListRef.getNode().scrollToIndex(); }
o do not know if this will help you... it scroll to a especific item in the list:
/*Example to Scroll to a specific position in scrollview*/
import React, { Component } from 'react';
//import react in our project
import {
View,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
Image,
TextInput,
} from 'react-native';
//import all the components we needed
export default class App extends Component {
constructor() {
super();
//Array of Item to add in Scrollview
this.items = [
'zero',
'one',
'two',
'three',
'four',
'five',
'six',
'seven',
'eight',
'nine',
'ten ',
'eleven',
'twelve',
'thirteen',
'fourteen',
'fifteen',
'sixteen',
'seventeen',
'eighteen',
'nineteen',
'twenty ',
'twenty-one',
'twenty-two',
'twenty-three',
'twenty-four',
'twenty-five',
'twenty-six',
'twenty-seven',
'twenty-eight',
'twenty-nine',
'thirty',
'thirty-one',
'thirty-two',
'thirty-three',
'thirty-four',
'thirty-five',
'thirty-six',
'thirty-seven',
'thirty-eight',
'thirty-nine',
'forty',
];
//Blank array to store the location of each item
this.arr = [];
this.state = { dynamicIndex: 0 };
}
downButtonHandler = () => {
if (this.arr.length >= this.state.dynamicIndex) {
// To Scroll to the index 5 element
this.scrollview_ref.scrollTo({
x: 0,
y: this.arr[this.state.dynamicIndex],
animated: true,
});
} else {
alert('Out of Max Index');
}
};
render() {
return (
<View style={styles.container}>
<View
style={{
flexDirection: 'row',
backgroundColor: '#1e73be',
padding: 5,
}}>
<TextInput
value={String(this.state.dynamicIndex)}
numericvalue
keyboardType={'numeric'}
onChangeText={dynamicIndex => this.setState({ dynamicIndex })}
placeholder={'Enter the index to scroll'}
style={{ flex: 1, backgroundColor: 'white', padding: 10 }}
/>
<TouchableOpacity
activeOpacity={0.5}
onPress={this.downButtonHandler}
style={{ padding: 15, backgroundColor: '#f4801e' }}>
<Text style={{ color: '#fff' }}>Go to Index</Text>
</TouchableOpacity>
</View>
<ScrollView
ref={ref => {
this.scrollview_ref = ref;
}}>
{/*Loop of JS which is like foreach loop*/}
{this.items.map((item, key) => (
//key is the index of the array
//item is the single item of the array
<View
key={key}
style={styles.item}
onLayout={event => {
const layout = event.nativeEvent.layout;
this.arr[key] = layout.y;
console.log('height:', layout.height);
console.log('width:', layout.width);
console.log('x:', layout.x);
console.log('y:', layout.y);
}}>
<Text style={styles.text}>
{key}. {item}
</Text>
<View style={styles.separator} />
</View>
))}
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 30,
},
separator: {
height: 1,
backgroundColor: '#707080',
width: '100%',
},
text: {
fontSize: 16,
color: '#606070',
padding: 10,
},
});
if i completly wrong, tell me...
Because ScrollView has no scrollToOffset function and It has only scrollTo function.
So let use function scrollTo with ScrollView or scrollToOffset with FlatList and it works normal.
If you are working with 'KeyboardAwareFlatList' this worked nicely:
https://github.com/APSL/react-native-keyboard-aware-scroll-view/issues/372
In short, use useRef and use the innerRef property of the KeyboardAwareFlatList rather than the ref property.
Currently, I'm having a problem with FlatList.
I have a component to render a list book.
By design, the header's width is the width of the screen and the body will be padding left and right 10px.
So I used contentContainerStyle={{paddingHorizontal: 10}}.
But The result is that the header and body are 10px padding left and right.
Please suggest a way to resolve it. Sorry for my bad English!!
Update: I'm so sorry for not describing my problem thoroughly.
In main.tsx
...
public render() {
return (
<FlatList
key...
data={..}
renderItem={this.renderItems}
ListHeaderComponent={this.renderHeader}
contentContainerStyle={styles.contentStyle}
/>
);
}
private renderHeader = () => {
return (
<View style={style.header}
//TODO something ...
</View>
);
}
private renderItems: ListRenderItem<IBook> = ({ item: {bookId} }) => bookId ?
(
<BookGridCell
title={...}
image={...}
//TODO more..
/>
) : <View style={styles.emptyBox} />
}
At renderItems, I called a component BookGridCell. In this component, the design of a book is set up. So if I directly add style inside renderItems, each book will have a left and right margin of 10px, not the entire body.
When use contentContainerStyle
with contenContainerStyle
When directly add style inside renderItems
with not use contentContainerStyle
Give a style to your body.
style={styles.bodyContainer}
and then inside StyleSheet add property.
const styles = StyleSheet.create({
bodyContainer: {
paddingHorizontal: 10
},
This is the correct way or
you can directly add padding inside your View.
style={{ paddingHorizontal: 10 }}
I have a minimal custom stateless component like this:
const ViewBox = (props) => (
<View style={ mainStyle : {backgroundColor: 'beige'} }>
{props.children}
</View>
)
export default ViewBox;
so I want to import and use it inside another component.
export default class Test extends React.Component {
render() {
return (
<View style={styles.containerView} >
<ViewBox style={styles.mainBox}>
<Text style={[styles.boxTitle, {color: '#8F468C'}]}>Lorem ipsum...</Text>
</ViewBox>
</View>
);
}
}
const styles = {
containerView: {
flex: 1,
marginTop: 50,
alignItems: 'center',
backgroundColor: 'brown',
},
mainBox: {
flex: 1,
width: 250, //No effect ! ! !
height: 250 //No effect ! ! !
},
boxTitle: {
backgroundColor: 'pink',
fontSize: 17,
marginTop: 20
}
};
Here we have at least 2 inexplicable facts:
1) and more important the width and height of the ViewBox (or every custom component you want to use here) is totally out of control! Assigning numeric size or Flex values has no effect and ViewBox keeps the minimum width/height needed to render the inner Text.
2) Removing the root View (so the ViewBox became the root) ViewBox continue ignoring any size, but now it fills all the space available.... WHY???
All mentioned behavoirs occurs using a custom View (ViewBox in this case), instead if replace it with a normal View all works as expected.
I guess to know enough flex and UI best practices for React-native, but such two cases are not well covered by docs. Hope somebody can surprise me!
This is actually how flexbox works:
Flex containers come with flexShrink: 1 by default meaning that they will shrink to their contents. Adding flexShrink: 0 changes that. You may need to use minWidth and minHeight with that instead.
The reason why it stretches is because there's nothing to tell it otherwise. Your container has the default alignItems: 'stretch' overwritten with alignItems: 'center' which shrinks its contents.
Have a look at the example code on Snack: https://snack.expo.io/B1VdUma1M
There's a really nice flexbox cheatsheet/playground that shows the behaviour at: https://yoksel.github.io/flex-cheatsheet/
Bear in mind React Native's implementation is slightly different.
In every code example mentioned in NativeBase Docs, there's no usage of React StyleSheet.
I didn't find a way to override styles of NativeBase.
How can I include React StyleSheet into my app?
NativeBase uses a different approach to add styling to your component.
It uses a Utility first approach, wherein it leverages the ReactNative stylesheets API and adds a layer of Utility Props on top of it. The idea is to pass props to your component to change their styling, instead of defining a className and adding styles in a separate stylesheet.
For example, with React Native, you would write:
function Example() {
return (
<View style={styles.container}>
{/* content */}
</View>;
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#0891b2',
paddingVertical: 16,
paddingHorizontal: 12,
borderRadius: 5,
alignSelf: 'center',
width: 375,
maxWidth: '100%'
}
});
Now, with NativeBase, to achieve the same view, you would write it as follows:
function Example() {
return (
<NativeBaseProvider>
<Box bg="primary.600" py="4" px="3" rounded="md" width={375} maxWidth="100%">
{/* content */}
</Box>
</NativeBaseProvider>;
);
}
Here, NativeBase has already defined bg, py, px, etc. as props and mapped them to the styling properties of backgroundColor, paddingVertical , paddingHorizontal, etc. respectively.
There is a list of these mappings provided here, which you can simply refer to add any styling that you may need for your component.
For NativeBase usually if you want customize the appearance you need to do it at a theme level and not a component/widget level.
http://nativebase.io/docs/v0.5.2/customize
I believe the reason it this keeps the UI consistent.
Have a look at the source code for all the widgets that you are trying to override the style for:
https://github.com/GeekyAnts/NativeBase/blob/master/Components/Widgets/
There are certain cases where you can amend certain aspects e.g. for the button
this.props.style is used anywhere but this.props.textStyle
So you can override the textStyle when you define the component:
<Button textStyle={{...}}/>
https://github.com/GeekyAnts/NativeBase/blob/master/Components/Widgets/Button.js
I want to directly update the value of a component due to performance reasons.
render(){
<View>
<Text style={styles.welcome} ref={component => this._text = component}>
Some Text
</Text>
<TouchableHighlight underlayColor='#88D4F5'
style={styles.button}>
<View>
<Text style={styles.buttonText}
onPress={this.useNativePropsToUpdate.bind(this)}>
Iam the Child
</Text>
</View>
</TouchableHighlight>
</View>
}
This is the method I use to update the text component. I dont know if I am setting the right attribute/ how to figure out which attribute to set:
useNativePropsToUpdate(){
this._text.setNativeProps({text: 'Updated using native props'});
}
Essentially trying to follow the same approach from this example:
https://rnplay.org/plays/pOI9bA
Edit:
When I attempt to explicitly assign the updated value:
this._text.props.children = "updated";
( I know this this the proper way of doing things in RN ). I get the error "Cannot assign to read only property 'children' of object'#'"
So maybe this is why it cant be updated in RN for some reason ?
Instead of attempting to change the content of <Text> component. I just replaced with <TextInput editable={false} defaultValue={this.state.initValue} /> and kept the rest of the code the same. If anyone know how you can change the value of <Text> using setNativeProps OR other method of direct manipulations. Post the answer and ill review and accept.
The text tag doesn't have a text prop, so
this._text.setNativeProps({ text: 'XXXX' })
doesn't work.
But the text tag has a style prop, so
this._text.setNativeProps({ style: { color: 'red' } })
works.
We can't use setNativeProps on the Text component, instead, we can workaround and achieve the same result by using TextInput in place of Text.
By putting pointerEvent='none' on the enclosing View we are disabling click and hence we can't edit the TextInput (You can also set editable={false} in TextInput to disbale editing)
Demo - Timer (Count changes after every 1 second)
import React, {Component} from 'react';
import {TextInput, StyleSheet, View} from 'react-native';
class Demo extends Component {
componentDidMount() {
let count = 0;
setInterval(() => {
count++;
if (this.ref) {
this.ref.setNativeProps({text: count.toString()});
}
}, 1000);
}
render() {
return (
<View style={styles.container} pointerEvents={'none'}>
<TextInput
ref={ref => (this.ref = ref)}
defaultValue={'0'}
// editable={false}
style={styles.textInput}
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 0.7,
justifyContent: 'center',
alignItems: 'center',
},
textInput: {
fontSize: 60,
width: '50%',
borderColor: 'grey',
borderWidth: 1,
aspectRatio: 1,
borderRadius: 8,
padding: 5,
textAlign: 'center',
},
});
export default Demo;
As setNativeProps not solving the purpose to alter the content of <Text />, I have used below approach and is working good. Create Simple React Component like below...
var Txt = React.createClass({
getInitialState:function(){
return {text:this.props.children};
},setText:function(txt){
this.setState({text:txt});
}
,
render:function(){
return <Text {...this.props}>{this.state.text}</Text>
}
});