I need to populate a menu with items from an api request.
I made some sample items; const items. Some of the menu items have children, and the children can also have children, so I need to nest several levels of menu items.
I made an Accordion() with Collapsible from react-native-collapsible and an AccordionItem() for items that have no children.
function App() renders the menu items twice. Once by manually adding Accordions and AccordionItems and once by mapping items[]. RootMenuItem() is called for each top level item and renders that item and its sub-items by recursion.
When manually adding each item it works the way I want it to. I need to populate the menu programatically, but nested accordions rendered by RootMenuItem() are misbehaving on android and iOS. When testing in Web on snack.io it seems to be working fine.
Here is a snack with my complete App.js:
https://snack.expo.dev/#dissar/nested-collapsibles
Am I doing something wrong?
Does anybody have any tips for doing this in a better way?
PS: The dynamically rendered items have weird widths when testing on snack.io, but don't worry about that.
I seem to have fixed it myself by removing the View on line 46 and 56;
function RootMenuItem({item}){
if(item.children.length > 0) {
return(
<View style={{flex: 1}} key={item.id}> // <---- I removed this
<Accordion item={item} style={styles.menuItemView}>
{
item.children.map(child => (
<View style={{paddingLeft: 18}} key={child.id}>
<RootMenuItem item={child} style={{paddingLeft: 10}}/>
</View>
))
}
</Accordion>
</View> // <---- Also removed this
)
}
else return (
<AccordionItem item={item}/>
)
}
Not really sure though why that View made the nested accordions not work as they should. Please let me know if you have the answer.
I have a better solution without using any 3rd party library. This is completely customised and easy to understand. I used the same format of data as you used.
first of all, we have a component
const [active, setActive] = useState(null);
return (
<ScrollView
style={{ marginTop: 50 }}
contentContainerStyle={styles.container}>
{arr.map((x, i) => (
<Item
key={x.name}
active={active}
i={i}
setActive={setActive}
child={x.child}
/>
))}
</ScrollView>
);
then for the list items and their child
function Item({ i, active, setActive, child }) {
const onPress = () => {
LayoutAnimation.easeInEaseOut();
setActive(i == active ? null : I);
};
const [childActive, setChildActive] = useState(null);
const open = active == I;
return (
<TouchableOpacity style={styles.item} onPress={onPress} activeOpacity={1}>
<View style={{ flexDirection: 'row', justifyContent: 'space-between' }}>
<Text>Header - {i + 1}</Text>
{child.length ? <Text>{open ? 'close' : 'open'}</Text> : null}
</View>
{open &&
child.map((x, i) => {
if (x.child.length) {
return (
<Item
key={x}
active={childActive}
i={i}
setActive={setChildActive}
child={x.child}
/>
);
}
return (
<Text key={x} style={styles.subItem}>
- SOME DATA
</Text>
);
})}
</TouchableOpacity>
);
}
It's a completely dynamic process, you can extend the chain as long as you want. You can also check out the expo to look at its works.
https://snack.expo.dev/#akash0208/forlorn-popsicle
export default function CardImageExample (props) {
const [path,setpath] = React.useState("'../collections/download-0.jpg'")
function change(){
console.log(path)
if(path == "'../collections/download.jpg'"){
setpath("'../collections/download-0.jpg'")
}
else{
setpath("'../collections/download.jpg'")
}
}
return (
<View style={{alignItems: 'center'}}>
<Card style={{height: 240, width: 280, alignItems: 'center'}}>
<CardItem bordered button onPress={change}>
**<Image source={require(path)} />**
</CardItem>
</Card>
</View>
);
}
the path variable does not work but if i use the path directly it works..
Please save
try passing require inside the path state and then in Image source pass only path instead of require(path)
you can check this other similar question, I think it will help you:
React Native - Image Require Module using Dynamic Names
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.
I'm using react-native-simple-modal for displaying models in android devices. Here the model always opens at the center of the screen. Setting the initial offset value in constructor has no effect. However moveUp function is working well. How to show the model at the top of the screen initially? Moreover I need to put the model just below a component.
CODE:
constructor(props) {
super(props);
this.state = {
open: false,
offset: -200,
};
}
moveUp = () => this.setState({offset: -200})
openModal = () => this.setState({open: true})
closeModal = () => this.setState({open: false})
render() {
return (
<View style={{flex: 1, justifyContent: 'flex-start', alignItems: 'center'}}>
<TouchableOpacity onPress={this.openModal}>
<Text>Open modal</Text>
</TouchableOpacity>
<Modal
offset={this.state.offset}
open={this.state.open}
modalDidOpen={this.modalDidOpen}
modalDidClose={this.modalDidClose}>
<View style={{alignItems: 'center'}}>
<Text style={{fontSize: 20, marginBottom: 10}}>Hello world!</Text>
<TouchableOpacity
onPress={this.moveUp}>
<Text>Move modal up</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.resetPosition}>
<Text>Reset modal position</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={this.closeModal}>
<Text>Close modal</Text>
</TouchableOpacity>
</View>
</Modal>
</View>
)
};
Currently there is no option to align your modal according to your offset in a initial stage. I just went through the internal code of react-native-simple-modal and found out that on the first time this doesn't understand the offset sent through the props. If you want this to be implemented then you can ask the author of this module to update the following code or you can just merge a PR on this module.
Inside the main index.js file of react-native-simple-modal you can see that the default state offset is being set as 0 instead of understanding it through the offset sent from props. So change the below line as:-
state = {
opacity: new Animated.Value(0),
scale: new Animated.Value(0.8),
offset: new Animated.Value(0)
};
to the code below:-
state = {
opacity: new Animated.Value(0),
scale: new Animated.Value(0.8),
offset: new Animated.Value(this.props.offset) ===>> update this initial value.
};
This offset works fine after the initial phase because the author of the module has understood the props offset in the componentWillReceiveProps lifecycle hook of the component. So just ask the author to update this or you can just raise the PR for this. I have checked this by updating the value in node_modules and if you just want to check if its working according to your requirement then you can also update it once and check that out.
I hope this will help you and still if you have any problem then let me know.
How should one implement the "X" to clear button in react native so that it works with Android as well as iOS. iOS has the text input option of "clearButtonMode" enum('never', 'while-editing', 'unless-editing', 'always').
To make it cross platform, do we need to just add an android conditional rendering of the clear button? Something like:
{Platform.OS === 'android' && <ClearTextButton />}
Seems a bit hacky so I am wondering if there is a cleaner method for this.
For your problem, you just need to create a simple button to handle the clear function of your input field and place it right next to your TextInput component to have the effect of clearButtonMode.
A naive implementation could be something like this:
Create these states in your main component constructor :
A state for the status of your TextInput (is it touched?, does it have text yet?)
A state for the actual value of your TextInput, set your TextInput's value to this state.
For example:
this.state = {
textInput1Status: 'untouched',
textInput1Value: '',
};
Create callback functions to set your states:
Create a callback function to set both your TextInput's value state and status state and assign it to the onChange prop of you TextInput.
For example:
<TextInput
onChangeText={(text) => this.onTextInput1Change(text)}
value={this.state.textInput1Value}
/>
...
onTextInput1Change(text) {
this.setState({
textInput1Status: 'touched',
textInput1Value: text
});
}
Create your own button using TouchableOpacity and handle the clear function.
For example:
<TouchableOpacity onPress={this.clearText}>
<Image
style={styles.button}
source={require('./myButton.png')}
/>
</TouchableOpacity>
...
clearText() {
this.setState({
textInput1Status: 'untouched',
textInput1Value: '',
});
}
Handle the rendering of your "X" button:
For example:
renderClearButotn() {
if (this.state.textInput1Status == 'touched') {
return (
<TouchableOpacity onPress={this.clearText}>
<Image
style={styles.button}
source={require('./myButton.png')}
/>
</TouchableOpacity>
);
} else {
return '';
}
}
...
render() {
return (
<TextInput
onChangeText={(text) => this.onTextInput1Change(text)}
value={this.state.textInput1Value}
/>
{this.renderClearButton()}
);
}
In this way your code will be independent from both iOS and Android. I hope this could help you!
There is another simple solution I found from this article. It works perfect for me in Android, and it is expected to give the same view and behavior in iOS also.
I had to modify the styles slightly to match with my UI
closeButtonParent: {
justifyContent: 'center',
alignItems: 'center',
borderTopRightRadius: 5,
borderBottomRightRadius: 5,
backgroundColor: "#cdcdcd",
width: 30,
},
Code credit goes to https://www.codevscolor.com/react-native-text-input-clear-button/ auther
This solution works ok but it's not the exact same effect than the clearButtonMode in iOS. The clearButtonMode won't dismiss the keyboard when clicked, and this solution for android will dispatch the Keyboard.dismiss event natively and there's no way to catch it, so the user needs to tap again on the input to get the keyboard back.