My problem
I render a list of simple items (numbers or strings) using Array.map
method. I use Modal
to add/update items. However, every opening or closing of the modal makes react
rerender the whole array, even if the array remains unmodified. I feel this is an expected behavior.
Questions
- Is it possible not to rerender the whole component when opening or closing the modal?
- What is the common way to add/update new items in array without rerendering the entire list?
Thanks, guys
Minimal code example
/* Console output:
* ---------------
* ROOT: render component
* -> ITEM: render 1
* -> ITEM: render 2
* -> ITEM: render 3 (not in map)
* ROOT: open modal
* ROOT: render component
* -> ITEM: render 1
* -> ITEM: render 2
* -> ITEM: render 3 (not in map)
* MODAL: close Modal
* ROOT: render component
* -> ITEM: render 1
* -> ITEM: render 2
* -> ITEM: render 3 (not in map)
*/
import * as React from 'react';
import {useState} from 'react';
import {View, Text, Modal, Pressable, StyleSheet} from 'react-native';
const items = [1, 2];
const Item = ({el}) => {
console.log(`-> ITEM: render ${el}`);
return <Text>Item: {el}</Text>;
};
const Testing = () => {
const [visible, setVisible] = useState(false);
const openModal = () => {
console.log('ROOT: open modal');
setVisible(true);
};
console.log("ROOT: render component");
return (
<View style={styles.wrapper}>
{/* Render simple list */}
<Text style={styles.header}>All items:</Text>
{items.map(el => (
<Item el={el} key={el} />
))}
<Item el={'3 (not in map)'} />
{/* Button to open modal */}
<Pressable style={styles.button} onPress={openModal}>
<Text style={styles.header}>Tap me to open modal</Text>
</Pressable>
{/*The Modal*/}
<Modal
animationType="slide"
transparent={false}
visible={visible}
onRequestClose={() => {
console.log('MODAL: close Modal');
setVisible(false);
}}>
<Text style={styles.header}>No content here...</Text>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
wrapper: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
button: {
borderRadius: 5,
backgroundColor: '#0aa',
marginVertical: 10,
padding: 10,
},
header: {
fontSize: 18,
fontWeight: '700',
},
});
export default Testing;
CodePudding user response:
The reason it re-renders is because you are updating state (setVisible).
You can useRef.
useState is directly tied with a components lifecycle; useRef is not, meaning you can change it and it won't trigger a re-render.
Example:
const visible = useRef()
//your code
useEffect(() => {
visible.current = false //initiate to false or true
depending on what you need.
}, [])
<Modal
animationType="slide"
transparent={false}
visible={visible}
onRequestClose={() => {
console.log('MODAL: close Modal');
visible.current = !visible.current //will toggle between true and false
}}>
<Text style={styles.header}>No content here...</Text>
</Modal>
CodePudding user response:
What I've learned today
Once we create a component, React
is tracking its state and performs rerendering if the state changes. Rerendering includes all the children of the component. Thus, I can answer my questions.
Answer to question 1
Yes. We need visible
is not in the state of the component that we wouldn't like to rerender. To achieve this, the Modal should be implemented separately from our component that turns into the parent of Modal.
At the same time, we want that parent can make set Modal visible. It is here that a new-for-me react hook comes into a play: useImperativeHandle
.
The workflow is as follows. The parent forwards a reference ref
to Modal using a forwardRef
function that wrapps the Modal component. Then, Modal declares ref
as a handler that can be used by parent. This declaration and available porperties are provided via useImperativeHandler
hook. That's it.
Note, however, that imperative code using refs should be avoided in most cases, as official docs state.
Below is the code snippet as a memento for me. No rerender on Modal open/close at this time!
/* Console output:
* ---------------
* ROOT: render component
* -> ITEM: render 1
* -> ITEM: render 2
* -> ITEM: render 3 (not in map)
* ROOT: open modal
* MODAL: openMe called from parent component via ref
* MODAL: close Modal
*/
//
// Modal component
//
const _MyModal = (props, ref) => {
const [visible, setVisible] = useState(false);
const openMe = () => {
console.log('MODAL: openMe called from parent component via ref');
setVisible(true);
};
useImperativeHandle(ref, () => ({publicHandler: openMe}}), [openMe]));
return (<Modal>...</Modal>);
};
const MyModal = forwardRef(_MyModal);
//
// Testing component
//
const Testing = () => {
const modalRef = useRef(null);
const openModal = () => {
console.log('ROOT: open modal');
modalRef.current.publicHandler();
};
// Rest of code without <Modal> tag
}
Answer to question 1 using redux
If you use redux
, there is no need in useImperativeHandle
hook. Just connect Modal component to store
to share visible
and action creator actSetVisible
, while connect the parent component to share only this action creator actSetVisible
. Everything works similarly as illustrated above.
However, if you do want to use useImperativeHandle
, you should point redux
to the fact that you're forwarding a reference, when connecting Modal to store:
const MyModal = connect(mapS2P, mapD2P, null, {forwardRef: true}))(forwardRef(_MyModal));
Answer to question 2
The suggestions from above showed how to get rid of excessive rerendering when modal opens or closes. My second question implied that I do something newbily wrong in react because I'm really a newby here. Therefore, in my humble opinion, using Modals is a good way to add elements to lists. In my case, it was an array of composed components rather than an array of simple strings, and rerendering was a critical issue.
Afterword
Live a century, learn a century, and you'll die a fool. Hope, this will help to anyone.