Grandchild component not changing grandparent state - reactjs

There is a button 2 layers deep in a NavigatorIOS that when triggered, should change the TabBarIOS selected TabBarIOS Item.
The components are structured as follows:
--FooterTabs
------NavigatorIOS:List -> ListItem
I am attempting to do the above by having a function inside FooterTabs that changes the state, and having TabBar.Items whose prop 'selected' becomes true when the state's 'selectedTab'=== different strings.
Like so:
_changeToAlarms(){
console.log('Hello from Footer Tabs');
this.setState({
selectedTab: 'Alarms'
});
}
render(){
return(
<Icon.TabBarItemIOS title={'Alarms'}
iconName='ios-clock'
selected={this.state.selectedTab === 'Alarms'}
onPress={()=> {
this.setState({
selectedTab: 'Alarms'
});
}}>
<ComingSoon/>
</Icon.TabBarItemIOS>
<Icon.TabBarItemIOS title={'Schedules'}
iconName='ios-moon'
selected={this.state.selectedTab === 'Schedules'}
onPress={()=> {
this.setState({
selectedTab: 'Schedules'
});
}}>
</Icon.TabBarItemIOS>
);
}
*Icon.TabBatItem acts exactly like TabBarIOS.Item.
onPress works, but to be able to change the tab in a similar fashion (by modifying the component's state) I passed _changeToAlarms() as a prop to 'SleepSchedules'.
<Icon.TabBarItemIOS ...
>
<NavigatorIOS
initialRoute={{
component: SleepSchedules ,
title: ' ',
passProps: {changeToAlarms: ()=> this._changeToAlarms()}
}}
style={{flex: 1}}
/>
</Icon.TabBarItemIOS>
And from SleepSchedules, I am navigating to the next component and passing the previous'changeToAlarms' as a prop.
_handlePress(selectedSchedule){
this.setState({selectedSchedule});
this._navScheduleItem(selectedSchedule)
}
_navScheduleItem(scheduleName){
this.props.navigator.push({
title: `${scheduleName} Sleep`,
component: ScheduleItem,
passProps: {scheduleName}, changeToAlarms: ()=> this.props.changeToAlarms
})
}
render(){
return(
...
onPress={()=> this._handlePress('Monophasic')}>
);
}
And in ScheduleItem I am attempting to call the prop 'changeToAlarm' that was passed, which should be the one from Footer Tabs.
_handleTouch(){
console.log('Hello from Schedule Item');
this.props.changeToAlarms;
}
render(){
return(
...
onPress={()=> this._handleTouch()}
);
}
The console logs 'Hello from ScheduleItem' every time I press it, but doesn't log 'Hello from FooterTabs' nor does it change the tab.
Does anyone spot an error?
I am a beginner, so thank you much for your help! :)

You need a () after this.props.changeToAlarms. this.props.changeToAlarms()

I think there might be a better approach but without seeing more code its hard to get the full picture of whats going on. On thing that did catch my eye was in your _handlePress function. You called setState and then another function.
According to ReactDocs:
There is no guarantee of synchronous operation of calls to setState
and calls may be batched for performance gains.
This might be causing an issue for you, again without more code its hard to tell. You can try using the overload method for setState that takes a function as the second arg.
setState(nextState, callback)
Performs a shallow merge of nextState into current state. This is the primary > method you use to trigger UI updates from event handlers and server request > > callbacks.
The first argument can be an object (containing zero or more keys to
update) or a function (of state and props) that returns an object
containing keys to update.
This ensures your setstate completes before your next function is executed.
_handlePress(selectedSchedule){
this.setState({selectedSchedule}, () =>
{
this._navScheduleItem(selectedSchedule);
});
}

Related

Encountering stale state when trying to pass state to sibling component

I'm building a simple to do list, and the code I'm trying to execute here is that when each task is clicked ('Task' component), it updates the parent state ('taskEdit') with the current task object. When taskEdit changes, a useEffect would run to re-render the whole component so that the parent can pass that object to another component ('EditTask' component) which is a modal that allows the user to edit the task, AND setEditModalVisible(true) would be run so that the Edit Task modal can be shown.
However, the issue is that when I do this, the 'EditTask' component renders with the previous state.
I.e. I click task A, and then Edit Task renders with an empty task object ({}), i.e. the initial state.
Then I click task B, and then Edit Task renders with the task A object.
Code is below. Any help is much appreciated!
Parent Component - taskEdit state is initialized here, and setTaskEdit is passed to the child Task component. When it's updated, I intend to pass the updated taskEdit state to the Edit Task component
When I press the task, in addition to updating the task object, it would update taskEditTrigger to true, which would allow the setEditModalVisible to continue - then I'd set it to false so that any further renders wouldn't trigger editModalVisible
const [editModalVisible, setEditModalVisible] = useState(false);
const [taskEditTrigger, setTaskEditTrigger] = useState(false);
const [taskEdit, setTaskEdit] = useState({});
useEffect(() => {
if (taskEditTrigger) {
setEditModalVisible(true)
setTaskEditTrigger(false)
} else {
setTaskEditTrigger(false)
}
}, [taskEdit]);
const renderItem = ({ item }) => (
<Task
item={item}
setTaskEdit={setTaskEdit}
setTaskEditTrigger={setTaskEditTrigger}
/>
);
return (
<View>
<FlatList
data={toDos}
renderItem={renderItem}
/>
<EditTask
item={taskEdit}
editModalVisible={editModalVisible}
setEditModalVisible={setEditModalVisible}
/>
</View>
Task Component - on pressing the task component, I intended to update the taskEdit state in the parent component with the current task
const Task = ({ item, setTaskEdit, setTaskEditTrigger })
const onPressTask = () => {
console.log(item.text)
console.log('Task is rendering')
if (item.isComplete) {
setIsTaskCompleteAlertVisible(true)
} else {
setTaskEdit(item)
setTaskEditTrigger(true)
}
};
return (
<View>
<TouchableOpacity onPress={onPressTask}>
<Text>{item.text}</Text>
</TouchableOpacity>
</View>
Export default memo(Task, (prevProps, nextProps) => {
If (prevProps.item.id === nextProps.item.id &&
prevProps.item.text === nextProps.item.text)
{
return true
}
return false
})
**Edit Task Component - I then intended to pass the item={taskEdit} into the Edit Task component to show the relevant task information **
function EditTask({
item, editModalVisible, setEditModalVisible}) {
console.log(item)
const [editingText, setEditingText] = useState(item.text);
const onPressEditTask = () => {
setEditModalVisible(false)
setTaskEdit({})
};
return (
<Modal visible={editModalVisible}>
<TouchableOpacity onPress={onPressEditTask}>
<Text>{editingText}</Text>
</TouchableOpacity>
</Modal>
Thanks to all the comments - found the reason for this issue.
I was able to lift the lift the taskEdit state from the Task component and pass it down from the parent component to EditTask successfully.
However, in EditTask I was updating the editingText state using the item.text property - and that was what was stale. When I updated the editingText state directly in Task and passed it by the same way to the Edit Task component, it was no longer stale.

setState() block the UI when two setState() run

Here is my componentDidMount() method :
componentDidMount() {
const subscription = accelerometer.subscribe(({ x, y, z, timestamp }) => {
x = Math.trunc(x*100);
this.setState({x})
});
}
In above method, every 100 millisecond state is changing. I used that state in my render() method as below :
render() {
const animatedImageStyle = StyleSheet.flatten([
styles.captureButton,
{
transform: [{rotateZ:this.state.x + 'deg'}]
}
])
return (
<SideMenu
menu={leftMenu}
isOpen={this.state.isOpenLeftMenu}
menuPosition={'left'}
bounceBackOnOverdraw={false}
onChange={(isOpenLeftMenu) => this.updateLeftMenuState(isOpenLeftMenu)}
>
<View>
<TouchableOpacity
activeOpacity={0.5}
onPress={(this.state.recordingMode == 'camera')?() => this.takePicture():() => this.toggleRecording()}
>
<Image
source={require('../assets/imgs/forRotate.png')}
style={animatedImageStyle}
/>
</TouchableOpacity>
</View>
</SideMenu>
)
}
Now, the problem is that when I trying to open sidemenu, it is not opening, I mean it opening but hanging too much. My whole app hanging too much.
I think that's because of below method :
updateLeftMenuState(isMenuOpen) {
this.setState({
isOpenLeftMenu:isMenuOpen
})
}
Notice that I am updating another state called isOpenLeftMenu, which may blocked during I update state x.
Can anyone tell me what't going wrong here ?
you can move the animation view in a separate component along with subscription logic. So the state update of that component won't affect the SideMenu component.

How to properly update/re-render a component that is not a child React Native?

I'm using a react-navigation. More specifically, I have a materialTabNavigator nested inside of a drawerNavigator. Each tab is in itself a stackNavigator. I have a button in homeScreen, that navigates to makePost.js. There I take in information and store it to Async storage using a simple wrapper.
In Posts.js there's a FlatList displaying each post as a component. The data for the FlatList is initially set correctly after making a request from Async Storage. The problem is that this only happens when the app is first opened. I have tried many different approaches to solve this. The only way so far I've found is to continuously setState in ComponentDidUpdate() in Posts.js. Obviously this is problematic, because it re-renders constantly. I can set a flag to stop is from rendering, but then it will not re-render again.
Ultimately, what I'd like to happen is that when I hit the user is done entering their information and is ready to make a post, they hit the button in makePost.js, and the data in the FlatList of Posts.js is update.
I've tried to pass parameters using navigation, does not work, parameters get lost somewhere, probably because of the nested navigators.
I could really used some guidance on the proper way to accomplish this.
( Navigators; not sure why this is forcing to one line )
---drawer
--tabNav
-home
homeScreen.js
makePost.js
-posts
posts.js
-messages
--drawer1
--drawer2
//Posts.js
export default class Posts extends React.Component {
state = {
rows: [
{id: 0, text: "dog"},
],
}
componentDidMount() {
this.loadState();
}
loadState = () => {
var value = store.get('posts').then((res => {
if (res === null) {
res = [{id: 0, text: "default"}]
} else {
res = res
}
this.setState({rows: res})
}))
}
componentDidUpdate() {
this.loadState();
}
renderItem = ({item}) => {
return (
<BoardTab style={styles.row} />
)}
render() {
return (
<View style={styles.view}>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<BoardScreenFooter />
</View>
);
}
And Posts.js button looks like this:
<TouchableOpacity
onPress={ () => {
this._onPressButton
this.storeFunc(this.state.newPost)
const retval = this.state.rows
this.props.navigation.navigate('Board',
{rowsID: retval});
}
}>
<Icon
reverse
name='md-camera'
type='ionicon'
color='green'
size={12}
/>
</TouchableOpacity>
storeFunc(newObj) {
newObj.id = newObj.id + 1
store.push('posts', newObj)
store.get('posts').then((res) => {
this.setState({rows: res})
})
}
Rapidly, i would say: use Redux. It alloq you to have global state in your app, which mean you can access the state anywhere (And also set them anywhere)
When opening the app, you get the data from the AsyncStore into the Redux store. You listen to the redux state (Which will be a props in your component) and display your list. When modifying your list in the other tab, you need to do 2 things:
Store the new data in the AsyncStorage
Update the state in the redux store. Since Posts.js will be listening at the redux store (as a props), it will re-render each time your data will change
A simple way to re-render a React-Navigation screen view on navigating to it:
All credit goes to Andrei Pfeiffer, Jul 2018, in his article: "Handle Tab changes in React Navigation v2" https://itnext.io/handle-tab-changes-in-react-navigation-v2-faeadc2f2ffe
I will reiterate it here in case the above link goes dead.
Simply add a NavigationEvents component to your render function with the desired listener prop:
render() {
return (
<View style={styles.view}>
<NavigationEvents
onWillFocus={payload => {
console.log("will focus", payload);
this.loadState();
}}
/>
<FlatList
ListFooterComponent={this.renderFooter}
style={styles.container}
data={this.state.rows}
renderItem={this.renderItem}
keyExtractor={extractKey}
>
</FlatList>
<PostScreenFooter />
</View>
);
}

How to render list of instances without losing state/recreating component?

Experimenting with making my own React router with some animations. Hit a brick wall.
I'm rendering a stack of screens.
The stack can be popped or pushed.
My problem is that when the stack changes the state is lost and the constructor is called again destroying the previous state (making the stack useless) .
How would I do this?
Create the screen (After this we push to the stack which is on the state)
/**
* Create a new React.Element as a screen and pass props.
*/
createNewScreen = (screenName: string, props: ?any = {}): any => {
// Props is not an object.
if (typeof props !== 'object') {
Logger.error(`Passed props to screen name ${screenName} wasn't an object or undefined. Will error next screens.`);
return;
}
let propsUnlocked = JSON.parse(JSON.stringify(props));
// Add unique screen it.
const uniqueScreenKey = this.generateRandomUniqueID();
propsUnlocked.key = uniqueScreenKey;
propsUnlocked.screenId = uniqueScreenKey;
propsUnlocked.navKing = this;
propsUnlocked.screenName = screenName;
// Find the original screen to copy from.
// This just copies the 'type'
// $FlowFixMe
return React.createElement(this.findScreenNameComponent(screenName).type, propsUnlocked);
}
Render the screens
render() {
return ( <View
{...this.props}
onLayout={(event) => this.onLayout(event)}
pointerEvents={this.state.isAnimating ? 'none' : undefined}
>
{ this.renderStackOfScreens() }
</View>);
};
renderStackOfScreens() {
// Render screens.
return this.state.stackOfScreens
.map((eachScreen, index) => {
// Render second last screen IF animating. Basically because we have a screen animating over the top.
if (index === this.state.stackOfScreens.length - 2 && this.state.isAnimating) {
return (
<Animated.View
key={eachScreen.props.screenId + '_parent'}
style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
{ eachScreen }
</Animated.View>
);
}
// Render last screen which is animated.
if (index === this.state.stackOfScreens.length - 1) {
return (
<Animated.View
key={eachScreen.props.screenId + '_parent'}
style={this.getOffset(this.state.animatedScreenOffset)}>
{ eachScreen }
</Animated.View>
);
}
})
// Remove the undefined values.
.filter((eachScreen) => !!eachScreen);
}
Can see the full example here
https://pastebin.com/BbazipKt
Screen types are passed in as unique children.
Once a component is unmounted, its state is gone forever. You might think "well I have a variable reference to the component so even though it's unmounted it still keeps its state, right?" Nope, React doesn't work that way. Unmounting a component is tantamount to destroying it. Even if you remount the "same" component again, as far as React is concerned it's a brand new component, with a brand new constructor call, mounting lifecycle, etc. So you need to abandon your approach of keeping the React components themselves in arrays as the history stack.
Frustrating, I know. Believe me, I've run into the same problem.
The solution is to pull out your View/Screen states from the components themselves and lift them into a parent. In essence, you're keeping the states in an array in the parent, and then passing them as props into the Views/Screens themselves. This might seem like a counter-intuitive, "non-Reactful" way of doing things, but it actually is in line with how React is intended to be used. State should generally be "lifted up" to the level of the closest common ancestor that all components will need access to it from. In this case, you need access to your state at a level above the Views/Screens themselves, so you need to lift it up.
Here's some pseudocode to illustrate.
Right now, your app seems to be structured sorta like this:
// App state
state: {
// stackOfScreens is React components.
// This won't work if you're trying to persist state!
stackOfScreens: [
<Screen />,
<Screen />,
<Screen />
]
}
// App render function
render() {
return <div>
{
this.state.stackOfScreens.map((ea, i) => {
return <View key={i} >{ea}</View>
}
}
</div>
}
Instead it should be like this:
// App state
state: {
// stackOfScreens is an array of JS objects.
// They hold your state in a place that is persistent,
// so you can modify it and render the resulting
// React components arbitrarily
stackOfScreens: [
{
name: "screen#1",
foo: "Some sort of 'screen' state",
bar: "More state,
baz: "etc."
},
{
name: "screen#2",
foo: "Some sort of 'screen' state",
bar: "More state,
baz: "etc."
},
{
name: "screen#3",
foo: "Some sort of 'screen' state",
bar: "More state,
baz: "etc."
},
]
}
// App render function
render() {
return <div>
{
this.state.stackOfScreens.map((ea, i) => {
return <View key={i} >
<Screen stateData={ea} callback={this.screenCallback} />
</View>
}
}
</div>
}
Notice the addition of a callback prop on the Screen components that you're rendering. Thats so that you can trigger changes to the rendered Screen "state" (which is actually tracked in the parent) from within the Screen.

How to change react element (li) with onClick to be strikethrough?

I've been trying to set element to become strikethrough when I click on it, but unfortunately I couldn't, nothing happens.
var UserList = React.createClass({
getInitialState: function() {
return {
user: [],
firstName: '',
lastName: '',
createdAt: 0,
isClicked: false,
};
},
handleOnClick: function() {
var isClicked = this.state.isClicked;
var style = {textDecoration: 'none'};
if (isClicked === true) {
style = {textDecoration: 'line-through'}
}
},
render: function() {
return (
<div>
<Users user={this.state.user} onClick={this.handleOnClick}/>
</div>
);
You can do it this way:
A similar example to yours:
const TodoItem = ({item, checkHandler}) => {
const itemCheckHandler = () => {
checkHandler (item.id);
};
return (
<div>
<li
style={{
textDecoration: item.checked ? 'line-through' : 'none',
}}
onClick={itemCheckHandler}
>
{item.text}
</li>
</div>
);
};
and your checkHandler in your App.js where the state resides is like this (items bein an array of items):
checkHandler = id => {
this.setState ({
items: this.state.items.map (item => {
if (item.id === id) {
item.checked = !item.checked;
}
return item;
}),
});
};
Don't try to change the style in a click handler. You should not change the style when a user does an action but rather do it at the time of it being rendered, that's the correct approach.
Store the "strikethrough" value in a flag in the state and do it in the render function.
For example:
getInitialState: function () {
return {
...
isStrikeThrough: false,
...
}
},
onHandleClick: function () {
....
// toggle the strikethrough state
this.setState({isStrikeThrough: !this.state.isStrikeThrough});
....
},
render: function () {
return (
<div>
<User
user={this.state.user}
strikeThrough={this.state.isStrikeThrough}
onClick={this.handleOnClick}
/>
</div>
);
},
You haven't given any details about the User component, so the explanation above is based solely on what we have in the question. That said, there are a couple of ways in which this could be improved.
First, I'm assuming that you can add the strikethrough flag to the User component and render the <strike>...</strike> (or comparable CSS styles) there. That may or may not be true (ie. if the User component is a third-party component, it may be difficult to change it).
Second, the strikethrough state described above looks to me like it ought to be internal to the User component. If all you're doing is changing the markup in the User component based on a click on the User component, then the strikethrough code ought to be in the User component. And, perhaps more importantly, if the strikethrough is supposed to represent something important about the state of a user, something that should be saved as part of the user's state, then the strikethrough flag ought to be part of the user's state (and have a more informative name than isStrikeThrough).
Dodek you can see the above answers but I think you need to change the way you look and think about a react application then it helps you a lot during your coding with react. The code you provided looks like a jQuery approach to me that you directly modify the DOM element when user do an action. Even if there was no issue in your approach still your code does not apply 'line-through' style to an already checked element which you get them from backend unless user clicks on an item.
You should look at your component as a very simple actor in a movie that accepts a very small set of parameters (compared to real word) and based on this input parameters it changes the way it appears in the frame. For example lets say you have a Todo item component (Like the one #Vennessa has provided here) in a vey simple case it accepts only an item text and also whether or not the item is checked;
These parameters may come from internal state or come from props or any other resources but in the end your component is accepting these parameters and all your internal logic that determines how your component should look must only rely and work with these params.
Try this out:
function Item(props){
const [clicked, setIsClicked] = useState(false);
function handleClick(){
setIsClicked((prevValue) => {
return (!prevValue)
});
}
return <li style={{textDecoration: clicked ? "line-through": "none" }} onClick={handleClick}>
{props.text} </li>
}

Resources