Encountering stale state when trying to pass state to sibling component - reactjs

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.

Related

Unable to prevent Flatlist from re-rendering (already using useCallback and memo)

I'm building a to do list app as part of a coding course, using Firebase Realtime Database and React Native with Expo.
I have no problems rendering the to do list, and in this case clicking a checkbox to indicate whether the task is prioritized or not.
However, each time I click on the checkbox to change the priority of a single task in the to do list, the entire Flatlist re-renders.
Each task object is as follows:
{id: ***, text: ***, priority: ***}
Task Component: (It consists of the text of the to do (task.text), and also a checkbox to indicate whether the task is prioritized or not). I've wrapped this component in React.memo, and the only props passed down from Todolist to Task are the individual task, but it still re-renders every time. (I left out most of the standard imports in the code snippet below)
import { CheckBox } from '#rneui/themed';
const Task = ({
item,
}) => {
console.log(item)
const { user } = useContext(AuthContext);
const onPressPriority = async () => {
await update(ref(database, `users/${user}/tasks/${item.id}`), {
priority: !item.priority,
});
};
return (
<View
style={{ flexDirection: 'row', alignItems: 'center', width: '95%' }}
>
<View
style={{ width: '90%' }}
>
<Text>{item.text}</Text>
</View>
<View
style={{ width: '10%' }}
>
<CheckBox
checked={item.priority}
checkedColor="#00a152"
iconType="material-community"
checkedIcon="checkbox-marked"
uncheckedIcon={'checkbox-blank-outline'}
onPress={onPressPriority}
/>
</View>
</View>
)}
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item !== nextProps.item) {
return true
}
return false
})
To Do List parent component: Contains the Flatlist which renders a list of the Task components. It also contains a useEffect to update the tasks state based on changes to the Firebase database, and the function (memoizedOnPressPriority) to update the task.priority value when the Checkbox in the task component is clicked. Since memoizedOnPressPriority is passed a prop to , I've tried to place it in a useCallback, but is still re-rendering all items when the checkbox is clicked. (I left out most of the standard imports in the code snippet below)
export default function Home2() {
const { user } = useContext(AuthContext);
const [tasks, setTasks] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
const tasksCopy = [];
for (let id in todos) {
tasksCopy.push({ ...todos[id], id: id });
}
setTasks(tasksCopy);
});
} else {
setTasks([]);
}
}, [user]);
const renderItem = ({ item }) => (
<TaskTwo
item={item}
/>
);
return (
<View style={styles.container}>
<FlatList
data={tasks}
initialNumToRender={5}
windowSize={4}
renderItem={renderItem}
keyExtractor={item => item.id}
/>
</View>
);
}
Could anyone let me know what I'm doing wrong, and how I can prevent the entire Flatlist from re-rendering each time I invoke the memoizedOnPressPriority function passed down to the Task component from the TodoList parent component? Any help is much appreciated!
The flamegraph for the render is below:
Update: I moved the prioritize function (memoizedOnPressPriority) into the Task component and removed the useCallback - so it's not being passed as a prop anymore. The re-render still happens whenever I press it.
Update 2: I added a key extractor , and also a custom equality function into the memoized task component. Still keeps rendering!
I'm not familiar with Firebase Realtime Database, but if I understand the logic correctly, the whole tasks array is updated when one item changes, and this is what is triggering the list update.
Fixing the memo function
Wrapping the Task component in memo does not work because it performs a shallow comparison of the objects. The objects change each time the data is updated because a new tasks array with new objects is created, so the references of the objects are different.
See this post for more details.
To use memo, we have to pass a custom equality check function, that returns true if the component is the same with new props, like so:
export default memo(Task, (prevProps, nextProps) => {
if (prevProps.item.id === nextProps.item.id && prevProps.item.priority === nextProps.item.priority ) {
return true;
}
return false;
})
Note that is the text is modifiable, you'll want to check that too.
Alternative solution : read data from the Task component
This solution is recommended and takes full advantage of Firebase Realtime Database.
To update only the component that is updated, you need to pass an array of ids to your flatlist, and delegate the data reading to the child component.
It's a pattern I use with redux very often when I want to update a component without updating the whole flatlist.
I checked the documentation of Firebase Realtime Database, and they indeed encourage you to read data at the lowest level. If you have a large list with many properties, it's not performant to receive the whole list when only one item is updated. Under the hood, the front-end library manages the cache system automatically.
//TodoList parent Component
...
const [tasksIds, setTasksIds] = useState([]);
useEffect(() => {
if (user) {
return onValue(ref(database, `users/${user}/tasks`), (snapshot) => {
const todos = snapshot.val();
// Build an array of ids
const tasksIdsFromDb = todos.map((todo) => todo.id);
setTasksIds(tasksCopy);
});
} else {
setTasksIds([]);
}
}, [user]);
...
// keep the rest of the code and pass tasksIds instead of tasks to the flatlist
const Task = ({ taskId, memoizedOnPressPriority }) => {
const [task, setTask] = useState(null)
const { user } = useContext(AuthContext);
useEffect(() => {
if (user) {
// retrieve data by id so only the updated component will rerender
// I guess this will be something like this
return onValue(ref(database, `users/${user}/tasks/${taskId}`), (snapshot) => {
const todo = snapshot.val();
setTask(todo);
});
} else {
setTask(null);
}
}, [user]);
if (task === null) {
return null
}
// return the component like before

React - child not reverting to parent state when child state not saved

I fear that I've made a complete mess of my React structure.
I have a parent component/view that contains an object of attributes that are the key element of the content on the page. The ideal is to have a component that allows the user to update these attributes, and when they do so the content on the page updates. To avoid having the content update on every single attribute update/click, I want to implement a 'revert'/'save' button on the attribute update screen.
The problem I'm having is that I'm passing the state update function from the main parent to the child components, and the save is updating the parent and then the child switches out of 'edit mode' and displays the correct value, but when I click revert it doesn't update the parent (good) but it still maintains the local state of the child rather than rerendering to the 'true' parent state so the child implies it has been updated when it actually hasn't.
I'm a hobbyist React developer so I'm hoping that there is just something wrong with my setup that is easily rectifiable.
export const MainViewParent = () => {
const [playerAttributes, updatePlayerAttributes] = useState(basePlayerAttributes);
const revertPlayerAttributes = () => {
updatePlayerAttributes({...playerAttributes});
};
// Fill on first mount
useEffect(() => {
// Perform logic that uses attributes here to initialise page
}, []);
const adjustPlayerAttributes = (player, newAttributes) => {
// Update the particular attributes
playerAttributes[player] = newAttributes;
updatePlayerAttributes({...playerAttributes});
};
return (
<PlayerAttributes
playerAttributes={playerAttributes}
playerNames={["Player 1"]}
playerKeys={["player1"]}
updatePlayerAttributes={adjustPlayerAttributes} // Update the 'base' attributes, requires player key - uses function that then calls useState update function
revertPlayerAttributes={revertPlayerAttributes}
/>
);
}
// Child component one - renders another child component for each player
export const PlayerAttributes = ({
updatePlayerAttributes
}) => {
return (
<AnotherChildComponent />
);
};
// Child component 2
const AnotherChildComponent = ({ ...someThings, updatePlayerAttributes }) => {
const [editMode, updateEditMode] = useState(false); // Toggle for showing update graph elements
const [local, updateLocal] = useState({...playerAttributes}); // Store version of parent data to store changes prior to revert/save logic
// Just update the local state as the 'save' button hasn't been pressed
const updateAttributes = ( attributeGroup, attributeKey, attributeValue) => {
local[attributeGroup][attributeKey] = attributeValue;
updateLocal({...local});
};
const revertAttributes = () => {
// Need to update local back to the original state of playerAttributes
updateLocal(playerAttributes);
// revertPlayerAttributes();
updateEditMode(false); // Toggle edit mode back
};
const saveDetails = () => {
updatePlayerAttributes(playerKey, local);
updateEditMode(false);
};
return (
<FormElement
editMode={editMode}
// Should pass in a current value
playerAttributes={playerAttributes}
attributeOptions={[0.2, 0.3, 0.4, 0.5, 0.6]}
updateFunction={updateAttributes} // When you click update the local variable
/> // This handles conditional display of data depending on editMode above
);
}
// Child component 3...
const FormElement = ({ editMode, playerAttributes, attributeOptions, updateFunction, attributeGroup, attributeKey }) => {
if (editMode) {
return (
<div>
{attributeOptions.map(option =>
<div
key={option}
onClick={() => updateFunction(attributeGroup, attributeKey, option)}
>
{option)
</div>
)}
</div>
);
}
return (
<div>{//attribute of interest}</div>
);
};
I'm just a bit confused about as to why the revert button doesn't work here. My child displays and updates the information that is held in the parent but should be fully controlled through the passing of the function and variable defined in the useState call at the top of the component tree shouldn't it?
I've tried to cut down my awful code into something that can hopefully be debugged! I can't help but feel it is unnecessary complexity that is causing some issues here, or lifecycle things that I don't fully grasp. Any advice or solutions would be greatly appreciated!

How to prevent re-render of parent when child dispatches changes to global state

Premise:
i have a screen component, an accordion component, and that component has subitem components. these subitem components change global state to handle their checked/unchecked state.
as a sibling to the accordions, i have a component that reads the global state to display the checked-count, so the screen component is accessing the global state to decouple the state from being explicitly called by the checked-count component
Problem:
when the accordion sub-item dispatches changes to global state, the accordion re-renders, thus messing with the expanded/collapsed state.
This is happening because the screen component thats accessing the global state is re-rendering. since i want to continue to dole out this global state from the screen, and avoid explicitly accessing global state inside the component (effort to make it re-usable), how can i prevent the screen from re-rendering when the sub-item updates global state, but also propagate those changes for the sub-item & checked-count component to re-render?
Screen:
export function List() {
const{ state: { checkedItems }, dispatch } = useData()
const [queue, setQueue] = useState([
{id: '1'},
{id: '2'},
{id: '3'}
]);
const setChecked = (id, shouldCheck) => {
const type = shouldCheck ? 'checkItem' : 'uncheckItem'
console.log('[ DISPATCH ] id:', id, 'type:', type)
dispatch({type, payload: id})
}
return (
<View>
<Accordion>
{
items
.filter(item => item.type === 3)
.map(item => useMemo(() => <AccordionSubItem item={item} onPress={setChecked}/>))
}
</Accordion>
<CheckedCount indicator={checkedItems.length + ' selected'} visible={checkedItems.length > 0}>/
</View>
)
};
AccordionSubItem:
export default AccordionSubItem = ({ item, onPress}) => {
const { state: {checkedItems} } = useData()
return (
<View style={styles.container}>
<View style={styles.dataWrapper}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.subtitle}>{item.subtitle}</Text>
</View>
<TouchableOpacity onPress={() => onPress(item.id, !checkedItems.includes(item.id))} style={styles.dataWrapper}>
<Checkbox
status={checkedItems.includes(item.id) ? 'checked' : 'unchecked'}
color="white"
/>
</TouchableOpacity>
</View>
);
};
Note: the dispatch and state youre seeing is not the typical redux setup. but ultimately, state is still returned by a reducer, and dispatch is an async function
How can i prevent the screen re-render but also ensure the sub-item component and checked-count component update their state?
That's totally ok that your List component rerenders when it's inner state is changed. The problem here is your AccordionsubItems are not keyed. I believe keying them should fix the problem:
<Accordion>
{items
.filter(item => item.type === 3)
.map(item => <AccordionSubItem key={item.id} item={item} onPress={setChecked}/>)
}
</Accordion>
I've removed the useMemo here since it doesn't look to give any advantage in this place.

prevent child component to re-render below context provider with memo

I am using a context provider in React to share data across several components. However since a value gets changed from one of my subcomponents, it rerenders all of my other components which partly leads to performance issues. So I want to prevent my child components to rerender. I tried using React.memo() but it's still rendering whenever I set the state of the Context Provider.
const Authenticator = React.memo(() => {
const [myChat, setMyChat] = useContext(ChatContext);
console.log("rerender"); // gets called everytime on click
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
);
});
My Context Provider looks like this:
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([]);
return (
<ChatContext.Provider value={[myChat, setMyChat]}>
{children}
</ChatContext.Provider>
);
};
My App.js looks like this:
<ChatProvider>
<Authenticator />
</ChatProvider>
React.Memo doesn't help since you are calling the useContext hook which will cause the component to re-render every time the value from the provider changes. You should consider splitting your context into two separate contexts: one for the value, one for the state updater.
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([])
return (
<ChatDispatchContext.Provider value={setMyChat}>
<ChatValueContext.Provider value={myChat}>
{children}
</ChatValueContext.Provider>
</ChatDispatchContext.Provider>
)
}
Then, update your Authenticator component to the following:
const Authenticator = React.memo(() => {
const setMyChat = useContext(ChatDispatchContext)
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
)
})

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>
);
}

Resources