Reputation: 73
I might have asked this question in a confusing way, so I apologize. But I have a map function that renders a lot of text elements dynamically and also styles them based on state. However, the calculations used to create the elements in the first place seems really expensive performance-wise. So I would like to render them once, and then store the created elements in state, and yet still have the styling rerender when it needs to.
I tried storing these mapped elements in an array, but the styling variables inside of each component are set to a single value when the component is stored. So rerendering the page doesn't change the styling of these components even if the initial variables used to set their styles in state have changed.
import React, {useState} from 'react';
import { Text, View, StyleSheet } from 'react-native';
export default function App() {
let [redText, setRedText] = useState(['This', 'That'])
let [blueText, setBlueText] = useState(['The', 'Other'])
let str = 'This That And The Other'
let arr = str.split(" ")
let componentsArr = null
function firstRender() {
componentsArr = []
componentsArr.push(arr.map((el) => {
return (
<View style={styles.container}>
<Text style={redText.includes(el)
? styles.redText
: blueText.includes(el)
? styles.blueText
: styles.blackText}>
{el}
</Text>
</View>
)
}))
return componentsArr
}
return (
<View style={styles.container}>
{componentsArr ? componentsArr : firstRender()}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
blackText: {
color: "black"
},
redText: {
color: "red"
},
blueText: {
color: "blue"
}
});
Let's say I have some code like this that adds an onPress event to each element that will automatically change it to red. How can I do that without mapping through and creating the View and Text components from scratch?
When it is initially pushed into the array, all of the styling variables are set in stone. Is there some way to preserve the ternary operations?
Upvotes: 1
Views: 297
Reputation: 1331
This sounds like a good use case for memoization. If you want to prevent rendering of the list and its elements, unless styles change, you need to apply this in two places, the <View/>
wrapper containing el
and the entire list itself. Below is how I would apply it.
import React, { useState, memo, useEffect } from "react";
import { Text, View, StyleSheet } from "react-native";
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
blackText: {
color: "black",
},
redText: {
color: "red",
},
blueText: {
color: "blue",
},
});
const stylesAreEqual = (prevProps, nextProps) => {
//we only compare styles since we assume el prop wont change
return (
prevProps.redText === nextProps.redText &&
prevProps.blueText === nextProps.blueText
);
};
//this component only re-renders if redText or blueText change.
//if el changes, it doesnt re-render. If you want this behavior to change,
//remove the stylesAreEqual function from the memo callback
const ViewItem = ({ el, redText, blueText }) => {
//responds to any styles passed into the component
const propStyle = redText.includes(el)
? styles.redText
: blueText.includes(el)
? styles.blueText
: styles.blackText;
//if you want to control the styles indiviually for each view item without needing
// to change redText, or blue Text props. It can managed locally here
const [style, setStyle] = useState(propStyle);
const onPressEvent = () => {
//write any change to style on a press
setStyle({ color: "black" });
};
useEffect(() => {
//if you want to respond to changed passed from a higher up component.
// though consider using useContext for this if your handling anything other
// than primary types (i.e strings, boolean, etc.)
setStyle(propStyle);
}, [propStyle]);
return (
<View style={styles.container}>
<Text onPress={onPressEvent} style={style}>
{el}
</Text>
</View>
);
};
const MemoViewItem = ({ el, redText, blueText }) =>
memo(
<ViewItem el={el} redText={redText} blueText={blueText} />,
stylesAreEqual
);
const MemoList = ({ arr, redText, blueText }) =>
//this means that unless the props passed into this component change,
// it will not re-render, even if a component above it does for any case.
memo(
<>
{arr.map((el) => {
return <MemoViewItem el={el} redText={redText} blueText={blueText} />;
})}
</>
);
export default function App() {
let [redText, setRedText] = useState(["This", "That"]);
let [blueText, setBlueText] = useState(["The", "Other"]);
let str = "This That And The Other";
let arr = str.split(" ");
let componentsArr = null;
function firstRender() {
componentsArr = [];
componentsArr.push(
<MemoList arr={arr} blueText={blueText} redText={redText} />
);
return componentsArr;
}
return (
<View style={styles.container}>
{componentsArr ? componentsArr : firstRender()}
</View>
);
}
Since I'm unsure if you want to change styles from onpress events, or from a general state coming from a higher component, I've included both in this example. In addition, depending on your use case, you can modify this above, and test where you need memoization or not since adding it does add extra overhead if its not necessary.
As a note though, the only way to prevent the list from re-rendering at all (only once on mount), is to manage the styles in a local component
In this case, removing redText
and blueText
from the App component, and removing the props on every component down the tree. From there, you can manage the styles inside the ViewItem
component. Should this be the case, you can also remove the memo
function. Below is an example.
import React, { useState, memo } from "react";
import { Text, View, StyleSheet } from "react-native";
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
alignItems: "center",
justifyContent: "center",
},
blackText: {
color: "black",
},
redText: {
color: "red",
},
blueText: {
color: "blue",
},
});
const ViewItem = ({ el }) => {
//if you want to control the styles indiviually for each view item without needing
// to change redText, or blue Text props. It can managed locally here
const [redText, setRedText] = useState(["This", "That"]);
const [blueText, setBlueText] = useState(["The", "Other"]);
const style = redText.includes(el)
? styles.redText
: blueText.includes(el)
? styles.blueText
: styles.blackText;
//const [styles, setStyles] = useState({});
const onPressEvent = () => {
//write any change to style on a press
setRedText("Cool");
};
return (
<View style={styles.container}>
<Text onPress={onPressEvent} style={style}>
{el}
</Text>
</View>
);
};
//prevent list from re-rendering unless app prop changes
const MemoList = ({ arr }) => memo(
<>
{arr.map((el) => (
<ViewItem el={el} />
))}
</>
);
export default function App() {
let str = "This That And The Other";
let arr = str.split(" ");
let componentsArr = null;
function firstRender() {
componentsArr = [];
componentsArr.push(<MemoList arr={arr} />);
return componentsArr;
}
return (
<View style={styles.container}>
{componentsArr ? componentsArr : firstRender()}
</View>
);
}
Upvotes: 1
Reputation: 155
i'm not sure i understand well what you wanted to do but as i understood, every word in the text must manage it's own toggle color ? So here how i would go.
export const App = () => {
const [texts, setTexts] = useState(['This', 'That', 'And', 'The', 'Other']);
const renderTexts = () => {
return texts.map(text => (
<CustomTextColorToggle el={text} key={text} />
));
};
return (
<View style={styles.container}>
{renderTexts()}
</View>
);
}
// Here defaultColor is optional if you want to add some
// more logic
const CustomTextColorToggle = ({ defaultColor, el }) => {
const [color, setColor] = useState(defaultColor);
const styles = color === "red"
? styles.redText
: color === "blue"
? styles.blueText
: styles.blackText;
return (
<View style={styles.container}>
<Text style={styles}>
{el}
</Text>
</View>
);
};
Inside CustomTextColorToggle you can wrap the View
with a Pressable
to change the color using setColor
Upvotes: 1