React Native, State Changes, but Component Does not Properly Render - reactjs

I am working on my first React Native app, but feel very comfortable with React and React Native, which had me surprised regarding this snag.
Goal - Allow users to dynamically create tabs based on their input needs. Each tab reuses the same component which is a form. On press, the tab should display its corresponding form. The form works fine.
The Problem - The parent component won't render the correct form on tab press. The state updates, but the component remains on the first component selected. If I list the forms on one screen, one after the other, I can see that filling out one form, has no effect on the others (desired effect).
If I don't render a form component, it will work on the first tab press. This makes me wonder if React is not registering a change in the parent component because the child is always a copy of the other child components I desire displayed.
I approached this problem three ways with no luck.
Creating an array of components stored in the parent components state. Creating a new tab pushes a form component into the current array in state by grabbing that array, pushing a component into it and reseting state with that new array. I store a key in state that updates on a screen press. That key matches with an index in the component array to show the selected component with its corresponding tab.
Using componentDidUpdate to set a displayed component on the screen press similar to above, but instead of keying into the array in my render function, in the componentDidUpdate function manually setting a displayedView on each update of the component and using that in render.
Currently (code to follow) - Creating unique keys in state for each tab added that stores the corresponding component and using that to decipher which component to display.
I decided to put a ref on some child components. I used console.log before render to see if that correct component was being selected for display. It showed it was, but it was not appearing that way on the screen. I also added the tab number being clicked to the UI and I can see my state updating properly on each tab press.
Any help would be appreciated. Last resort I am going to have a user select the amount of forms they want before reaching this view and create it based on that number, but I would prefer not to.
import React from 'react';
import { connect } from 'react-redux';
import { View, Text, TouchableOpacity } from 'react-native';
import CreateWorkoutSet from './CreateWorkoutSet';
import { Entypo } from '#expo/vector-icons';
class CreateWorkout extends React.Component {
constructor(props) {
super(props)
this.state = {
workoutSetTemplate0: <CreateWorkoutSet navigation={this.props.navigation}/>,
showTab: 0,
totalTabs: 0,
}
this.addWorkoutSetToGroup = this.addWorkoutSetToGroup.bind(this)
}
componentDidMount() {
this.setState({
tabs: [
<TouchableOpacity onPress={() => this.setState({ showTab: 0 })}>
{ this.state.showTab === 0 ? (
<View style={{ backgroundColor: 'red', height: 40, width: 40 }}>
<Text>Tab: 1</Text>
</View>
) :
<View style={{ backgroundColor: 'grey', height: 40, width: 40 }}>
<Text>Tab: 1</Text>
</View>
}
</TouchableOpacity>
],
showTab: 0
})
}
addWorkoutSetToGroup() {
let tabNumber = this.state.totalTabs + 1
let tabKey = `workoutSetTemplate${tabNumber}`
let tabs = this.state.tabs
tabs.push(
<TouchableOpacity onPress={() => this.setState({ showTab: tabNumber })}>
{ this.state.showTab === tabNumber ? (
<View style={{ backgroundColor: 'red', height: 40, width: 40 }}>
<Text>Tab: {tabNumber}</Text>
</View>
) :
<View style={{ backgroundColor: 'grey', height: 40, width: 40 }}>
<Text>Tab: {tabNumber}</Text>
</View>
}
</TouchableOpacity>
)
this.setState({ [tabKey]: <CreateWorkoutSet navigation={this.props.navigation} />, tabs: tabs, totalTabs: tabNumber })
}
render() {
let template = this.state[`workoutSetTemplate${this.state.showTab}`]
let tabs = this.state.tabs
return(
<View style={{flex: 1, justifyContent: 'center', marginTop: 20}}>
<View style={{ flexDirection: 'row' }}>
{tabs}
</View>
<View>
<Text>Add Exercise To Group</Text>
<TouchableOpacity onPress={()=> this.addWorkoutSetToGroup()}>
<Entypo name='circle-with-plus' size={36} color='red' />
</TouchableOpacity>
</View>
<TouchableOpacity onPress={() => this.props.navigation.navigate('BlockDetail')}>
<Text>Blocks</Text>
</TouchableOpacity>
<Text>{this.state.showTab}</Text>
{template}
</View>
)
}
}
export default connect()(CreateWorkout)

Try calling
this.forceUpdate()
after updating the state

Related

Is there a way to have a button render my camera component in React Native?

I am new to react native and am trying to make my camera component pop up whenever I click on a button. I am able to get the camera to render in App.js, but the minute I try to get it rendering in the component it just doesn't work. Should I use state to get this to render? If so, why doesn't react native allow you to just render a component within a component? I'm trying to understand the concept of components calling other components. Heres my code:
import React, {Component} from 'react';
import {Button, StyleSheet, Text, View} from 'react-native';
import DeviceCamera from './camera';
class CameraButton extends Component {
render() {
return (
<View style={styles.container}>
<Text> Button to Open Camera </Text>
<Button
onPress={() => {
<DeviceCamera />;
}}
title="click me to open the camera!"
color="#841584"
/>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
instructions: {
textAlign: 'center',
color: '#333333',
marginBottom: 5,
},
});
export default CameraButton;
I was trying to use the on press function to call the camera component but perhaps I am misunderstanding something.
Yeah, I would probably just use state here
class CameraButton extends Component {
showCamera = () => this.setState({ showCamera: true });
render() {
return (
<View style={styles.container}>
<Text> Button to Open Camera </Text>
<Button
onPress={this.showCamera}
title="click me to open the camera!"
color="#841584"
/>
{this.state.showCamera && <DeviceCamera />}
</View>
);
}
}
With JSX, you can use foo && <Bar />; if foo evaluates to something truthy, then it will render your component, otherwise it will not.

Select items and transfer selected items to another page

Hello I am creating an app, and am having difficulties trying to create a way where the user selects multiple images, which will then be passed on to another screen. Could I please get some help on this?
Much will be appreciated.
So, the way my app works is that, the user selects multiple items, then there should be an add button or a save button, that will get the selected items and display them to another screen. The items have a value which are Images, not text. This is purely the reason why I asked the question here because most of the React-Native tutorials include values based on text, rather than Images.
The problem I am having, is trying to figure out a way for the user to select multiple items, and clicking a save button, which will in return transfer all of the "selected items" to another screen to be display there. Much like a viewer.
import React, { Component } from 'react';
import { Text, View, StyleSheet, AppRegistry, FlatList, Image, TouchableOpacity } from 'react-native';
import flatListData from '../database';
class FlatListItem extends Component {
static navigationOptions = ({ navigation }) => ({
title: 'FirstScreen!'
})
render() {
return (
<View style={{
flex: 1,
flexDirection:'column',
}}>
<View style={{
flex: 1,
flexDirection:'row',
}}>
<View style={{
flex: 1,
flexDirection:'column',
height: 100
}}>
<TouchableOpacity onPress={() => this.props.navigation.navigate('SecondScreen')} >
<Image source={{uri: this.props.item.imageUrl}}
style={{width: 100, height: 100, margin: 5}}></Image>
</TouchableOpacity>
</View>
</View>
<View style={{
height: 1,
backgroundColor:'white'
}}>
</View>
</View>
);
}
}
class FirstScreen extends Component {
static navigationOptions = ({ navigation }) => ({
title: 'First Screen'
})
render() {
return (
<View style={{flex: 1, marginTop: 22}}>
<FlatList
data={flatListData}
renderItem={({item, index})=>{
//console.log(`Item = ${JSON.stringify(item)}, index = ${index}`);
return (
<FlatListItem item={item} index={index}>
</FlatListItem>);
}}
>
</FlatList>
</View>
);
}
}
export default example;
const styles = StyleSheet.create({
flatListItem: {
color: 'white',
padding: 10,
fontSize: 16,
}
});
Since you did not provide any sample code, so I will try to suggest a way to handle via pseudocode
You can abstract out the list of images into a centralized helper class, then you render from this helper class for user to select.
Now when user have selected one of the image, you just need to capture the ID or any unique identifier, and pass it to second screen.
On this second screen, just using this ID/unique identifier that you've received and search it from the aforementioned centralized helper class and render it.
Looks like you have two things to figure out;
one is keeping track of what items a user has selected on your image selection screen
sending the data back between screens
Looks like you are most likely using react-navigation based on your example so the simplest solution would be to take advantage of React's state and use react-navigation's parameter passing between screens.
With react-navigation you can use the second argument in navigation.navigate to pass parameters/callbacks to the other screen. So you can navigate to a screen and pass a callback to it as such.
...
this.props.navigation.navigate(
'ItemSelectionScreen',
{ onSubmit: (items) => { /* Do something with items */ } }
)
...
And here is a basic example of a selection screen with some comments to explain how it works.
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: 'grey',
alignItems: 'center',
justifyContent: 'center'
}
});
class ItemSelectionScreen extends React.Component {
constructor(props) {
super(props);
this.onSubmit = () => props.navigation.getParam('onSubmit'); // Keep the passed callback once we have navigated to this screen
this.state = {
selectedItems: [] // Our initial selectedItems (empty)
};
}
handleToggle = (item, isSelected) => {
const { selectedItems } = this.state;
this.setState({ selectedItems: isSelected ? selectedItems.filter(ent => ent !== item) : [...selectedItems, item] }); // Toggle if an item is selected
};
handleSubmitAndExit = () => {
const { onSubmit } = this;
const { selectedItems } = this.state;
onSubmit(selectedItems); // Pass your selectedItems back to the other screen
this.props.navigation.goBack(); // And exit the screen
};
handleExit = () => {
this.props.navigation.goBack(); // Exit the screen without calling onSubmit
};
renderItem = (item, index) => {
const { selectedItems } = this.state;
const isSelected = selectedItems.some(ent => ent === item); // Determine if an item is selected
return (
<TouchableOpacity key={index} onPress={() => this.handleToggle(item, isSelected)}>
<Text>{`${isSelected ? 'X' : 'O'} ${item}`}</Text>
</TouchableOpacity>
);
};
render() {
return (
<View style={styles.container}>
{['item1', 'item2', 'item3'].map(this.renderItem)}
<TouchableOpacity onPress={this.handleSubmitAndExit}>
<Text>Submit and Exit</Text>
</TouchableOpacity>
<TouchableOpacity onPress={this.handleExit}>
<Text>Exit</Text>
</TouchableOpacity>
</View>
);
}
}
export default ItemSelectionScreen;
Good luck and hope this was helpful.

is there a way to change Button style direclty from a callback function onPress

I'm new in React Native, and I'm trying to make Button selected and the others just not selected, so as in web, I remove all selected class from all the buttons, then give the class to the selected one.
How can I do that here?
For your question, one way to solve it is maintaining a state in your component constructor like this: this.state={selected: 0}. The integer of the state selected will be the index of the currently pressed button among the buttons in the same container.
Assuming you are mapping out the buttons with something like
buttons.map((button, index) =>
<Button>
<Text>
Some Text
</Text>
</Button>)
You should be able to attach a key in every Button tag with the index. Then you can call a component method in each Button that makes use of the key to set the state's selected to the index of the button being pressed. And finally you can render different styles based on the currently selected button by checking with value in the state.
import React, { Component } from 'react'
import {
AppRegistry,
StyleSheet,
Text,
View,
Button
} from 'react-native'
class App extends Component {
constructor(props) {
super(props);
this.state = {buttonClick: false};
}
onClickButton() {
this.setState(previousState => {
return {buttonClick: !previousState.buttonClick};
});
}
render() {
const cssButtonStyle = (this.state.buttonClick)? 'darkslateblue' : 'mediumturquoise';
console.log(this.state.buttonClick);
return (
<View style={styles.container}>
<Text style={styles.welcome}>
Welcome to React Native!
</Text>
<Button
color={cssButtonStyle}
onPress={(event) => this.onClickButton(event)}
title="Learn More"
accessibilityLabel="Learn more about this purple button"
/>
</View>
)
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
},
welcome: {
fontSize: 20,
textAlign: 'center',
margin: 10,
},
})
AppRegistry.registerComponent('App', () => App)
This is what can be achieved actually with React-Native.
It has basic customizations only.(https://facebook.github.io/react-native/docs/button).
Use the above code in this place:
http://dabbott.github.io/react-native-web-player/
You can use this npm package for more customizations:
https://github.com/ide/react-native-button

react native search and scroll to hit

I need some help implementing a search and scroll to hit in react native. Did a lot of searches and ended up in some dead ends (found some refs examples I couldn't get to work).
Tried building this snippet as a kick-off:
https://snack.expo.io/#norfeldt/searching-and-scroll-to
import React, { Component } from 'react';
import { Text, View, ScrollView, TextInput, StyleSheet } from 'react-native';
export default class App extends Component {
state = {
text: '41'
}
render() {
return (
<View style={styles.container}>
<TextInput
style={{height: 60, borderColor: 'gray', borderWidth: 1, borderRadius: 10, margin: 5, padding:30, color: 'black', }}
onChangeText={(text) => this.setState({text})}
value={this.state.text}
/>
<ScrollView >
{[...Array(100)].map((_, i) => {return <Text style={styles.paragraph} key={i}>{i}</Text>})}
</ScrollView>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
paddingTop: 10,
backgroundColor: '#ecf0f1',
},
paragraph: {
margin: 10,
fontSize: 18,
fontWeight: 'bold',
textAlign: 'center',
color: '#34495e',
},
});
Any help getting started would be appreciated.
My guess would be:
You could start by binding a ref of your <ScrollView/>.
// ScrollView Ref.
<ScrollView ref={(ref) => this['ScrollView'] = ref}>
...
</ScrollView>
And each of your <Text/> components (by index).
// Text Refs.
<Text ref={(ref) => this[i] = ref} style={styles.paragraph} key={i}>{i}</Text>
You could then set a submit() function.
Said function could find the ref equal to this.state.text using a try catch statement for graceful failure in edge cases.
If found; target x and y offset could be retrieved using measure()
scrollTo() could then be called to scroll to the target component.
// Scroll To Query.
submit = () => {
try {
const { text } = this.state // Text.
const target = this[text] // Target.
// Locate Target.
target.measure((framex, framey, width, height, x, y) => {
// Scroll To Target.
this.ScrollView.scrollTo({x, y, animated: true})
})
} catch (error) {
return console.log(error)
}
}
First of all, I highly recommend you to use FlatList instead of ScrollView. There are a few reasons for this:
FlatList has much more optimized performance in comparison with ScrollView (in scroll view all items are rendered at once, regardless of the fact if they are visible on screen or not)
Moreover, the handling scrolling and rendered items is much simpler in FlatList, you do not need to know anything about x, y axis and pixels, you just work with indexes.
in order to have a comprehensive comparison between these two methods you may look at:
http://matthewsessions.com/2017/05/15/optimizing-list-render-performance.html
Now back to your question, as I said I suggest you to use FlatList, then everything will be as simple as a piece of a cake.
You can find modified example of your expo in:
https://snack.expo.io/HkMZS1SGz
The changes that you need to make in your code, include:
Instead of ScrollView use, FlatList, so change this:
<FlatList
ref={ref => {this.flatListRef = ref;}}
data={new Array(100).fill(0).map((item, index) => index)}
renderItem={({ item }) => (
<Text style={styles.paragraph}>
{item}
</Text>
)}
/>
If you are not already familiar with FlatList, you need to know, the data is added in data prop as an array (I added an array of 100 numbers), and the way it is rendered is given to FlatList as renderItemprop (I added the text with the same styling as you did).
Moreover, note that you do not need to pass ref to <Text>, because FlatList already knows about items that it contains. You just need to add a ref to the FlatList itself:
ref={ref => {this.flatListRef = ref;}}
Now when ever you want to make and scrolling, you can simple call scrollToIndex method of the FlatList, for example write a method called scrollHandler:
// Scroll to Query
scrollHandler = (itemIndex)=>{
this.flatListRef.scrollToIndex({animated: true, index: itemIndex});
}
just pay attention that,flatListRef is the name of the ref assigned to the FlatList.
now, when you want to perform scroll action, you can simply call this method. Forexample, modify your text input to:
<TextInput
style={{height: 60, borderColor: 'gray', borderWidth: 1,
borderRadius: 10, margin: 5, padding:30, color: 'black', }}
onChangeText={(text) => this.setState({text})}
value={this.state.text}
onSubmitEditing={()=>this.scrollHandler(this.state.text)}
/>
Steps
Remember every item's position with onLayout.
scrollTo() position when text input, and only if item found.
Code
const styles = StyleSheet.create({
textinput: {
borderBottomColor: 'purple',
textAlign: 'center',
borderBottomWidth: 2,
height: 40,
marginTop: 20,
},
text: {
textAlign: 'center',
fontSize: 16,
margin: 10,
}
});
export class App extends Component {
data = [];
datapos = {};
scrollref = null;
constructor(props) {
super(props);
/// make 100s example data
for (var i =0; i<100; ++i)
this.data.push(i);
this.state = {
inputvalue: '0'
}
}
render() {
return (
<View style={{flex: 1}}>
<TextInput style={styles.textinput}
value={this.state.inputvalue}
onChangeText={(text) => {
this.setState({inputvalue: text});
let y = this.datapos[+text];
y !== undefined && this.scrollref.scrollTo({ y, animated: true });
}}
/>
<ScrollView
ref={(ref) => this.scrollref = ref}
>
{
this.data.map( (data) => (
<Text style={styles.text}
key={data}
onLayout={(layout) => this.datapos[data] = layout.nativeEvent.layout.y}
>{data}</Text>
))
}
</ScrollView>
</View>
)
}
}
Result:

Update child components based on parent state

I'm new to react, I'm running into what I'm pretty sure is a common problem with a common solution but, because I'm new to the idea of React, I have no idea how to solve it.
Using the following as an example, how can I get my children to re-render whenever the TouchableWithoutFeedback callbacks are called? (Don't tell me to use a different component, this idea can be applied to lots of parent-child relationships - when something happens on a parent, re-render the children).
<TouchableWithoutFeedback
onPressIn={() => { /* what do I do here */ }}
onPressOut={() => { /* what do I do here */ }}
>
<View
style={{
backgroundColor: /* if touchable is highlighted black, otherwise white */
}}
>
<Text
style={{
color: /* if selected, then white, otherwise, black */
}}
>
Some Text
</Text>
</View>
</TouchableWithoutFeedback>
It seems a bit verbose to have to write custom components (so I can just call setState() every time I need this kind of functionality, do I really have to?)
Your onPressIn() and onPressOut() methods should update the state of your component. When a component's state is changed, the render() method is called and the component is re-rendered. If the component's children's properties have changed due to the update in state, then they will be re-rendered as well.
In your specific case you should do the following:
Add a state to your component. Somewhere in the component definition add state = { pressed: false }
Update the state with your onPress methods and use the state when setting the properties of your component's children:
<TouchableWithoutFeedback
onPressIn={() => { this.setState({pressed: true}) }}
onPressOut={() => { this.setState({pressed: false}) }}
>
<View
style={{
backgroundColor: (this.state.pressed) ? 'black' : 'white'
}}
>
<Text
style={{
color: (this.state.pressed) ? 'white' : 'black'
}}
>
Some Text
</Text>
</View>
</TouchableWithoutFeedback>
The above was tested in React Native 0.48 on an Android device.
As a rough example you would update the state on those callbacks and pass it as prop to your children components in the render method.
For example :
this.state = { isHighlighted: false }; // inside the parent component constructor
render() {
const { isHighlighted } = this.state;
return (
<div>
<FirstChild style={{ backgroundColor: isHighlighted ? 'black' : 'white' }}>
<SecondChild style={{ color: isHighlighted ? 'white' : 'black' }}>Some text</SecondChild>
</FirstChild>
</div>
);
}
and the callbacks would be :
onPressIn={() => { this.setState({ isHighlighted: true }) }}
onPressOut={() => { this.setState({ isHighlighted: false }) }}
If you would want to have style through nested components you would have to :
a) keep passing them as props
b) hold your state in a store (https://github.com/reactjs/redux)
In your constructor :
this.state={pressedIn:false,onPressOut:false}
Then
<TouchableWithoutFeedback
onPressIn={() => { this.setState({pressedIn:true,pressedOut:false}}
onPressOut={() => { {this.setState({pressedOut:true,pressedIn:false}}
>
<View
style={{
backgroundColor: this.state.pressedIn? "someColor":"SomeOtherColor"
}}
>
<Text
style={{
color: this.state.onPressOut? "someColor":"SomeOtherColor"
}}
>
Some Text
</Text>
</View>
</TouchableWithoutFeedback>

Resources