Update child components based on parent state - reactjs

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>

Related

Conditional rendering cannot be seen due to a return before them

Please help give me a better title, I couldn't word what I am asking.
To understand my question I have to provide some context of my project. Originally I wanted to conditionally render two pages through two buttons. Button A rendering screen A and button B rendering screen B. After figuring out how to pass the state from a parent component to a child and its child etc, I changed my button to a sliding animation for better design.
This causes issues because now when a new screen is rendered, the animation does not show because it is simply re-rendered with the original starting place in the animation(I rendered the slider through each screen). I thought about providing two different sliders, each starting in the opposing opposition but that would still lose the entire slide effect.
I have now resulted to rendering the Slider so it is there all the time and is not re-rendered. However I have realized now that if I return it before my conditionals, that code is never reached. I have provided a working demo that shows my problem perfectly as well as the code below(I only provided App.js, the rest is on the demo if needed). I want to render Slider in App.js.
The working demo is here, you can see the slider does not slide, it just changes screens. I need it to slide. Also the sliding animation only works on iphone so I would use that emulator rather than the web.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
whichComponentToShow: "Screen1"
};
}
goToMap = () => {
this.setState({ whichComponentToShow: "Screen2" });
};
goToList = () => {
this.setState({ whichComponentToShow: "Screen1" });
};
render() {
const { whichComponentToShow } = this.state;
/* This is how I thought I could render this, but obv it makes the rest of the code unreachable.
How can I render this and then have the conditional page below? Each time the new page renders,
it stops the animation from working due to rendering the new page.
return(
<Slider/>
)*/
if(this.state.whichComponentToShow === 'Screen1'){
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
<ListHome
renderMap = {this.goToMap.bind(this)}
renderList = {this.goToList.bind(this)}
/>
</View>
);
}
else if(this.state.whichComponentToShow === 'Screen2'){
return(
<View style={{backgroundColor: '#d1cfcf' ,flex: 1}}>
<MapHome
renderMap = {this.goToMap.bind(this)}
renderList = {this.goToList.bind(this)}
/>
</View>
);
}
Slider.js (wont show up on the snack apparently
const Slider = (props) => {
const [active, setActive] = useState(false)
let transformX = useRef(new Animated.Value(0)).current;
useEffect(() => {
if (active) {
Animated.timing(transformX, {
toValue: 1,
duration: 300,
useNativeDriver: true
}).start()
} else {
Animated.timing(transformX, {
toValue: 0,
duration: 300,
useNativeDriver: true
}).start()
}
}, [active]);
const rotationX = transformX.interpolate({
inputRange: [0, 1],
outputRange: [2, Dimensions.get('screen').width / 4]
})
return (
<SafeAreaView style={{
flex: 1,
alignItems: 'center'
}}>
<View style={{
flexDirection: 'row',
position: 'relative',
height: 45,
width: 240,
borderRadius: 10,
backgroundColor: 'white',
marginHorizontal: 5
}}>
<Animated.View
style={{
position: 'absolute',
height: 45 - 2*2,
top: 2,
bottom: 2,
borderRadius: 10,
width: Dimensions
.get('screen').width / 3 - 3.5 ,
transform: [
{
translateX: rotationX
}
],
backgroundColor: '#d1cfcf',
}}
>
</Animated.View>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(false); props.renderList() }}>
<Text>
List
</Text>
</TouchableOpacity>
<TouchableOpacity style={{
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}} onPress={() => {setActive(true); props.renderMap() }}>
<Text>
Map
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
export default Slider
I tried your expo snack and saw no code related to animation, so I'm assuming the code in the snack isn't your current code, and that you really do already have a working, animated, <Slider ... />.
In your situation, what you could do to keep your Slider rendered and not unmounted, is to use variables in the render() method.
Basically, you can assign the <Slider .../> JSX to a variable, and you can use that variable in another JSX part later.
Assigning a key to the specific JSX also helps guide React that this is the same component between render calls, so it also prevents unintentional rerenders of that component.
Here's an edit with comments from what you wrote in your post. I hope this makes sense.
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
whichComponentToShow: "Screen1"
};
}
goToMap = () => {
this.setState({ whichComponentToShow: "Screen2" });
};
goToList = () => {
this.setState({ whichComponentToShow: "Screen1" });
};
render() {
const { whichComponentToShow } = this.state;
/*
Keep the slider JSX in a variable to be used.
Setting a specific key also helps prevent it from being accidentally re-rendered in some conditions.
)*/
const sliderRender = <Slider key='slider' />;
if (this.state.whichComponentToShow === 'Screen1') {
return (
<View style={{ backgroundColor: '#d1cfcf', flex: 1 }}>
<ListHome
renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
/>
{/* Put the rendered slider into the render tree */}
{sliderRender}
</View>
);
}
else if (this.state.whichComponentToShow === 'Screen2') {
return (
<View style={{ backgroundColor: '#d1cfcf', flex: 1 }}>
<MapHome
renderMap={this.goToMap.bind(this)}
renderList={this.goToList.bind(this)}
/>
{/* Put the rendered slider into the render tree */}
{sliderRender}
</View>
);
}
}
}
Edit : Expo Snack demonstrating it working

Dealing With Multiple Flatlists on the same Screen

Please help my deal with unnecessary re-renders when having two flatlists on the same screen
My screen requires two flatlists-
For HOSTS
For QUEUE
When the component mounts, I get data from the api call like this-
{
"hosts": [{"id":"1", "name":"kyouma"},...],
"queue": [{"id":"99", "name":"eren"},...]
}
Now what I do is I store my hosts and queue separately in my redux store like this-
this.props.dispatch({
type: GET_ROOM_HOSTS,
payload: info['hosts']
})
this.props.dispatch({
type: GET_ROOM_QUEUE,
payload: info['queue']
})
where info is the object received from the api call as shown above. Now I mapStateToProps these two from the redux store to the default screen component such that-
this.props.roomQueue is for queue and
this.props.roomHosts is for hosts
My FlatList's are like this-
<FlatList
data={this.props.roomQueue}
horizontal={true}
keyExtractor = {item => item.id}
renderItem({item} => {
return(
<customComponent (with suitable props) ..../>
)
})
/>
<FlatList
data={this.props.roomHosts}
numColumns={3}
keyExtractor = {item => item.id}
renderItem({item} => {
return(
<customComponent (with suitable props) ..../>
)
})
/>
PLEASE NOTE that both the FlatList's are present in the same Component (React.Component) of the screen and are displayed at different parts of the screen(queue at bottom of the screen and hosts at the top of the screen). Also queue and hosts are independent of each other. Screen looks like this
My problem is that even if there is a change in this.props.roomQueue, the FlatList having its data={this.props.roomHosts} get's re-rendered.
How do i prevent this re-render to ensure that only if the FlatList's corresponding data changes, then only will it re-render, otherwise it won't. Do I have to change the way I store queue and hosts? or is there something else?
You can do this with using only one flatlist. Merge your both array's into one and show results from one list.. you can spare them in ui with a type.
This is a genuine procedure of what developers do, cz rendering 2 list in same page and same direction is accually no mean. Your query is valid.
You can add List ListFooterComponent and it will automatically do this for you
<FlatList
contentContainerStyle={{
width: WINDOW_WIDTH,
paddingVertical:WINDOW_WIDTH*0.2,
marginLeft:10
}}
ListFooterComponent={()=> returnYourViewDesignHere()}
columnWrapperStyle={{ flex: 1, justifyContent: "space-around" }}
keyExtractor={(item) => item.id}
onEndReached={() => getPaginationData()}
onEndReachedThreshold={0.0001}
numColumns={3}
showsVerticalScrollIndicator={false}
data={allShows}
renderItem={({ item, index }) => {
return (
<TouchableWithoutFeedback
key={item.index + Math.floor(Math.random() * 1000)}
onPress={() =>
props.navigation.navigate(
item.type == "movie" ? "MovieDetailScreen" : "SeasonDetail",
{
data: item,
object: {
id: item.id,
},
}
)
}
>
<View style={styles.boxContainer}>
<View style={styles.imageBackground}>
<Text style={styles.backgroundText}>KEIN</Text>
<Text
style={[styles.backgroundText, { color: COLOR.primary }]}
>
POSTER
</Text>
</View>
<Image
source={{
uri: item.coverUrl ? item.coverUrl : item.coverPath,
}}
style={styles.imageBox}
resizeMode={"stretch"}
/>
<Text
numberOfLines={2}
ellipsizeMode={"tail"}
style={styles.text}
>
{item.showTitle ? item.showTitle : item.title}
</Text>
{userWatchedList.some((uwl) => uwl.id == item.id) ? (
<TouchableWithoutFeedback
onPress={() =>
isloggedIn
? removeFromUserWatchList(item)
: handleModalVisibility()
}
>
<Image
source={WATCHLIST_CHECKED}
style={{
width: 25,
height: 25,
position: "absolute",
right: 5,
top: 5,
}}
/>
</TouchableWithoutFeedback>
) : (
<TouchableWithoutFeedback
onPress={() =>
isloggedIn
? addToUserWatchList(item)
: handleModalVisibility()
}
>
<Image
source={CIRCLE_UNCHECKED}
style={{
width: 25,
height: 25,
position: "absolute",
right: 5,
top: 5,
}}
/>
</TouchableWithoutFeedback>
)}
</View>
</TouchableWithoutFeedback>
);
}}
/>

onLoadEnd is not fired in react-native Image

hi am trying to load an remote image. onLoadStart is hitting but not the onLoadEnd
`
<View style={{ paddingTop: 60, paddingBottom: 10 }}>
{this.state.loading ? (
<DotIndicator size={25} color={"white"} />
) : (
<Image
resizeMode={this.resizeMode}
style={[styles.imageStyle, this.tintStyle]}
onLoadStart={e => {
this.setState({ loading: true });
}}
onLoadEnd={e => this.setState({ loading: false })}
// defaultSource={NoProfile}
// loadingIndicatorSource={require("#images/profile_placeholder.png")}
source={this.userImageUri}
onError={error => {
this.tintStyle = { tintColor: "lightgray" };
this.resizeMode = "contain";
this.userImageUri = NoProfile;
}}
/>
)}
</View>
`
EDIT 1
onLoadStart is hit. onLoad is also never being called
does any one have a clue. am new to react.
Any help is appreciated.
thanks is advance
SOLUTION
Since Vignesh and hong mentioned the image is never there so its on loadEnd will never be called. So instead of loading only image or loader I loaded the loader on top of image. Posting this here as it may be useful for someone at sometime. Once again thanks to Vignesh and hong
<View
style={{
padding: 10,
width: WIDTH - 50,
height: WIDTH - 25,
alignSelf: "center"
}}
>
{this.state.loading ? (
<MaterialIndicator
size={50}
color={"red"}
style={{
marginTop: WIDTH / 2,
alignSelf: "center"
}}
/>
) : null}
<Image
resizeMode={this.resizeMode}
style={[styles.imageStyle, this.tintStyle]}
onLoadStart={e => {
this.setState({ loading: true });
}}
onLoad={e => {
this.setState({ loading: false });
}}
onLoadEnd={e => this.setState({ loading: false })}
// defaultSource={NoProfile}
// loadingIndicatorSource={require("#images/profile_placeholder.png")}
source={this.userImageUri}
onError={error => {
this.tintStyle = { tintColor: "lightgray" };
this.resizeMode = "contain";
this.userImageUri = NoProfile;
}}
/>
</View>
Let's say that the value of this.state.loading was false before the first render.
When the first render happens, this.state.loading ? returns the Image component, onLoadStart is triggered and this.state.loading is set to true.
When the second render happens, this.state.loading is found to be true and this.state.loading ? returns the DotIndicator component. All the hard work done by the Image component during the previous the render is lost. In fact, Image component was never present in that context.
Hence, onLoadingEnd will never be triggered, because Image component never appeared in the second render.
And the DotIndicator will forever go round and round and round... Waiting for it's lost love..
If loading is true from the beginning, nothing will be called in the image.
If loading value is false at first, the image does not see loadStart running, only load function and loadEnd function will be called.
This is because the loadStart function runs when the start value is false and it is already rendered. And if the start value is true, nothing is done because it does not draw an image.
This is a very simple example that you can experiment with:
import React, { Component } from 'react';
import { View, Image } from 'react-native';
export default class App extends Component {
state={
loading: false,
}
render() {
return (
<View>
{this.state.loading ? (
<Image
style={{width: 100, height: 100}}
source={{uri: 'https://facebook.github.io/react-native/img/tiny_logo.png'}}
/>
) : (
<Image
style={{width: 50, height: 51}}
onLoadStart={e => this.setState({ loading: true })}
onLoad={e => alert("onLoad")}
onLoadEnd={e => alert("onLoadEnd")}
source={require('#expo/snack-static/react-native-logo.png')}
/>)}
</View>
);
}
}
I ended up copying the same image but set its style to width: 1, height: 1. this will always be displayed but is not visible. What this allows is to continue to set the state despite the many rerenders. I then in a if statement have the actual image or a text component that will display image not found. By default NoImageFound is set to true. Here is my code
<Image
source={{ uri: image }}
style={{ width: 1, height: 1 }}
onLoadStart={() => setNoImageFound(true)}
onLoad={() => setNoImageFound(false)}
/>
{noImageFound ? (
<View style={{ justifyContent: "center", alignItems: "center" }}>
<Text style={{ textAlign: "center" }}>No image found</Text>
</View>
) : (
<View style={styles.icon}>
<Image source={{ uri: image }} style={styles.image} />
</View>
)}

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

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

React Native Dynamic Checkbox Inside Dynamic checkbox

I have a app developed in React native and I need make a dinamic checkbox list inside other dynamic checkbox list, the first list works fine, but when i try to make the other dynamic checkbox list inside, this not show totally.
this is the code:
<View style ={styles.inside2}>
{
this.state.categorias.map((categoria) => {
return(
<View key={categoria.id}>
<CheckBox
title={categoria.nombre}
iconRight
checkedColor='red'
uncheckedColor='red'
checked={this.state.checkCategorias[categoria.id-1]}
containerStyle ={{backgroundColor: '#f7f7f7', borderColor: '#f7f7f7' }}
textStyle = {{fontSize: 12, color: '#787878' }}
checkedIcon = 'dot-circle-o'
uncheckedIcon = 'circle-o'
onPress = {(checked) => this.cambioCheckCat(categoria.id)}
/>
{
this.state.checkCategorias[categoria.id-1] ?
<View
style ={{backgroundColor: 'lightgray', width: '100%', height: '100%'}}
key ={categoria.id+'t'} >
<Text style = {styles.titulo2}>
Seleccione una o varias sub-categorias de {categoria.nombre}
</Text>
<View style={styles.separador} />
{
this.state.subcategorias.map((subcategoria) => {
if(subcategoria.id_categoria == categoria.id){
return(
<CheckBox
key={subcategoria.id+'s'}
title={subcategoria.nombre}
iconRight
checkedColor='red'
uncheckedColor='red'
checked={this.state.checkSubCategorias[i]}
containerStyle ={{backgroundColor: '#f7f7f7', borderColor: '#f7f7f7' }}
textStyle = {{fontSize: 12, color: '#787878' }}
checkedIcon = 'dot-circle-o'
uncheckedIcon = 'circle-o'
onPress = {(checked) => this.cambioCheckSub(subcategoria.id)}
/>
)
}
})
}
</View> : null
}
</View>
)
})
}
</View>
This show fine until the line "Selecciona una o varias categorias...", after i have a .map but this part simply not show.
EDIT:
Sorry I see my error, I have a get the data bad from the database, but the code works fine so I left this here in case somebody need make a dynamic into other dynamic.
Sorry I see my error, I have a get the data bad from the database, the code works fine, I left this here in case somebody need make a dynamic into other dynamic.

Resources