Storing dynamically rendered page elements in state, and keeping variables functional? - javascript

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?

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

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

Related

Array of text from a JSON object inserted into <Text> elements via forEach/map not rendering in React Native

I'm making a Choose Your Own Adventure style game in React Native to learn how everything works. I figured it makes sense to keep the story in a separate JSON file and render it on one screen that updates depending on what arc the user is in.
Problem: when I write a forEach or map or other function to go through the array of paragraphs, the part of the screen it's supposed to be on is blank. Text from other parts of the return area displays fine, but what is supposed to be displayed from the forEach does not. Here's my code:
const StoryDisplayScreen = props => {
const theStory = require('../data/TestStory.json')
const theArc = 'intro'
const storyText = () => {
theStory[theArc].story.forEach((paragraph, i) => {
<Text style={styles.body}>{paragraph}</Text>
})
}
return (
<View style={styles.screen}>
<ScrollView>
<View>
{storyText()}
</View>
<View style={styles.promptNextArcArea}>
<TouchableOpacity style={styles.promptNextArc}>
<Text style={styles.promptNextArcText}>What do you do?</Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
)
}
In case you're wondering the structure of the JSON file and its contents, I'm using this one to test with: https://github.com/gophercises/cyoa/blob/master/gopher.json
I've tried using map instead, tried putting things in Views, tried putting the forEach/map functions directly into the return section of the code, tried console.log-ing to confirm that my functions are working properly (which they appear to be), and... yeah, I'm at a loss.
Any suggestions?
Consider using map instead of forEach.
Full Working Example: Expo Snack
import * as React from 'react';
import {
Text,
View,
StyleSheet,
ScrollView,
TouchableOpacity,
} from 'react-native';
import Constants from 'expo-constants';
import { Card } from 'react-native-paper';
const StoryDisplayScreen = (props) => {
const theStory = require('./data/TestStory.json');
const theArc = 'intro';
const storyText = () => {
return theStory[theArc].story.map((paragraph, i) => (
<Card style={{margin: 5, padding:5}}>
<Text style={styles.body}>{paragraph}</Text>
</Card>
));
};
return (
<View style={styles.screen}>
<ScrollView>
<View>{storyText()}</View>
<View style={styles.promptNextArcArea}>
<TouchableOpacity style={styles.promptNextArc}>
<Text style={styles.promptNextArcText}>What do you do?</Text>
</TouchableOpacity>
</View>
</ScrollView>
</View>
);
};
export default StoryDisplayScreen;
const styles = StyleSheet.create({
screen: {
flex: 1,
paddingTop: Constants.statusBarHeight,
backgroundColor: '#ecf0f1',
padding: 8,
},
});
firstly, if you want your sort is a function, it must return something, just like the render method
const sort = () => {
return (<View></View>)
}
you also can make the sort is a view, use it as a variable. like that, both they are can works.
const sort = (<View></View>)
but the sort must have something return. your way use for each and map dont return any. there are two ways can do that.
const storyText = () => {
//,first way,define a array, push it in the array
let storyViews= []
theStory[theArc].story.forEach((paragraph, i) => {
sortyviews.push(<Text style={styles.body}>{paragraph}</Text>)
})
return soortView;
}
the second way is to return directly a array
const storyText = () => {
//,first way,define a array, push it in the array
let storyViews= []
storyViews = theStory[theArc].story.map((paragraph, i) => {
return (<Text style={styles.body}>{paragraph}</Text>)
})
return soortView;
}
for more about the array operate to see the doc

react native rendering a list of buttons can't figure out what's wrong

I'm trying to make a list of buttons based on the input from user input type is an array of options like so multipleOptions = ['1', '2', '3'] then we loop through each option to show a Button can't figure out why it's not working here's my code :
import React, { useState } from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
const InputButton = (multipleOptions, likertScale, onPress) => {
const [options, setOptions] = useState([]);
if (likertScale) {
setOptions([...new Array(likertScale).keys()].map((i) => i));
} else if (multipleOptions) setOptions(multipleOptions);
return (
<View style={styles.container}>
{options ? (
options.map((option, i) => (
<View style={[styles.button]} key={`${i}`}>
<TouchableOpacity onPress={() => onPress(option)}>
<Text>{option}</Text>
</TouchableOpacity>
</View>
))
) : (
<Text>no options</Text>
)}
</View>
);
};
const App = () => {
return (
<View>
<InputButton multipleOptions={['1', '2','3']} />
</View>
)
}
const styles = StyleSheet.create({})
export default App;
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
},
button: {
margin: 3,
flex: 1,
backgroundColor: '#EEF6FA',
minHeight: 72,
borderRadius: 2,
justifyContent: 'center',
alignItems: 'center',
},
});
the error message is
Too many re-renders. React limits the number of renders to prevent an infinite loop.
or sometimes this
options.map is not a function
TypeError: options.map is not a function
at InputButton
(All kind of optimisations are welcome)
Thanks in Advance guys.
code demo https://snack.expo.io/#mansouriala/dac832
You put set state in every re-render so you get a loop. So you have two options use useEffect to just set state one time or set the first state directly.
https://snack.expo.io/ZvLQM9FEF
const InputButton = ({multipleOptions, likertScale, onPress}) => {
const [options, setOptions] = useState(likertScale?[...new Array(likertScale).keys()].map((i) => i):[ ...multipleOptions]);
return (
<View style={styles.container}>
{options ? (
options.map((option, i) => (
<View style={[styles.button]} key={`${i}`}>
<TouchableOpacity onPress={() => onPress(option)}>
<Text>{option}</Text>
</TouchableOpacity>
</View>
))
) : (
<Text>no options</Text>
)}
</View>
);
};
export default InputButton;
You have several issues here.
The first, which leads to Too many re-renders. React limits the number of renders to prevent an infinite loop. is because you're calling setOptions at each render, which triggers another render, etc… This is infinite loop, because when you're setting a state, React re-renders the component. To avoid that, you have to wrap your expression with useEffect and the correct dependencies.
React.useEffect(() => {
if (likertScale) {
setOptions([...new Array(likertScale).keys()].map((i) => i));
} else if (multipleOptions) {
setOptions(multipleOptions);
}, [multipleOptions, likertScale]);
This way, this expression would only run when multipleOptions or likertScale change.
https://reactjs.org/docs/hooks-reference.html#useeffect
The other problem is that InputButton props argument is wrong: you forgot the props destructuring. It should be const InputButton = ({ multipleOptions, likertScale, onPress }) => { /* body function */ }.
Finally, it's a bad practice to use an array index as a key prop, because array order could change. You should use a stable key, like the option value key={option}.
https://reactjs.org/docs/lists-and-keys.html#keys

Passing a local variable to a component in React Native

I am building a todo list app, and I want to long press individual todos, to change their color to green in order to mark them as finished.
I have a var color = 'white'; inside my App.js and I have another component named listItem for the list items.
I have this pretty basic function to change the color
const longPressHandler = () => {
(color == 'white') ? color = 'green' : color = 'white';
}
and I am sending the color via props of listItem
<ListItem item={item} longPressHandler = {longPressHandler} color={color} pressHandler = {pressHandler}/>
and I am using it as follows backgroundColor: props.color Check below:
const styles = StyleSheet.create({
listItem:{
padding: 8,
margin:4,
fontSize: 18,
textAlignVertical:'center',
borderColor:'gray',
borderWidth: 3,
borderStyle: 'solid',
borderRadius:10,
backgroundColor: props.color,
}
})
BUUT, it does not work... What am i doing wrong? Is there any simple solution that I am missing...
Here is the full code of App.js
import React, {useEffect, useState} from 'react';
import {Text, View, StyleSheet, FlatList, Alert, TouchableWithoutFeedback, Keyboard, Button, AsyncStorage } from 'react-native';
import ListItem from './components/listItem';
import AddItem from './components/addItem';
// npx react-native start // TO START
// npx react-native run-android // TO PROJECT INTO EMULATOR
//Hooks cant be used in class components, thus, switched from Class component structure => Function component structure
export default function TODOList() {
const [todos, setTodos] = useState([
{todo: 'do chores', key: '1'},
{todo: 'do homework', key: '2'},
{todo: 'go to grocery', key: '3'},
]);
var color = 'white';
//This handler DELETES the pressed list item from the list
const pressHandler = (key) => {
const newtodos = todos.filter(todo => todo.key != key);
setTodos(newtodos);
}
//ADDS a new todo with the given text and a randomly generated key to the old todos list
const inputSubmitHandler = (text) => {
if(text.length > 0){
const key = Math.random().toString();
const newTodos = [{text, key}, ...todos];
setTodos(newTodos);
}else{
Alert.alert('ERROR!', 'Text cannot be empty', [{text:'OK'}])
}
}
//TODO Change color of the individual item in the list
const longPressHandler = () => {
(color == 'white') ? color = 'green' : color = 'white';
}
console.log('color', color);
return (
<TouchableWithoutFeedback onPress={() => {Keyboard.dismiss();}}>
<View style={styles.mainPage}>
<View style = {styles.header}>
<Text style={styles.title}>TODO List</Text>
</View>
<View style={styles.content}>
<AddItem inputSubmitHandler={inputSubmitHandler} />
<View style={styles.list}>
<FlatList
data={todos}
renderItem={( {item} ) => (
<ListItem item={item} longPressHandler = {longPressHandler} color={color} pressHandler = {pressHandler}/>
)}
/>
</View>
</View>
</View>
</TouchableWithoutFeedback>
);
}
//The margins, paddings, etc. are given as pixel values, wont work same in other devices.
const styles = StyleSheet.create({
mainPage: {
flex: 1, // takes the whole background
backgroundColor: 'white',
},
content: {
flex: 1,
},
list:{
margin: 10,
flex:1,
},
header:{
height: 50,
paddingTop: 8,
backgroundColor: 'orange'
},
title:{
textAlign: 'center',
fontSize: 24,
fontWeight: 'bold',
},
});
Here is the full code of listItem.js
import React from 'react';
import {Text, View, StyleSheet, TouchableOpacity} from 'react-native';
export default function ListItem(props) {
//Moved the style inside of the function since I want to use the color prop in 'backgroundCcolor'
const styles = StyleSheet.create({
listItem:{
padding: 8,
margin:4,
fontSize: 18,
textAlignVertical:'center',
borderColor:'gray',
borderWidth: 3,
borderStyle: 'solid',
borderRadius:10,
backgroundColor: props.color,
}
})
return (
<TouchableOpacity onLongPress={() => props.longPressHandler()} onPress = {() => props.pressHandler(props.item.key)}>
<Text style={styles.listItem}> {props.item.todo}</Text>
</TouchableOpacity>
)
}
There are few changes you can do to the code
Move the color choice to the ListItem and pass a prop to decide that
No need to create the whole style inside the item itself you can pass the ones that you want to override
So to do this you will have to start with your listitem
<TouchableOpacity
onLongPress={() => props.longPressHandler(props.item.key)}
onPress={() => props.pressHandler(props.item.key)}>
<Text
style={[
// this will make sure that only one style object is created
styles.listItem,
{ backgroundColor: props.marked ? 'green' : 'white' },
]}>
{props.item.todo}
</Text>
</TouchableOpacity>
And your long press handler should change like below, this will set the marked property in the state which you use to decide the color above
const longPressHandler = (key) => {
const updatedTodos = [...todos];
const item = updatedTodos.find((x) => x.key == key);
item.marked = !item.marked;
setTodos(updatedTodos);
};
You can refer the below snack
https://snack.expo.io/#guruparan/todo
Try this way
const longPressHandler = (index) => {
const newTodos = [...todos];
newTodos[index].color = (newTodos[index].color && newTodos[index].color == 'green') ? 'white' : 'green';
setTodos(newTodos);
}
<FlatList
data={todos}
renderItem={( {item, index} ) => (
<ListItem
item={item}
index={index}
longPressHandler = {longPressHandler}
color={item.color || 'white'}
pressHandler = {pressHandler}
/>
)}
/>
export default function ListItem(props) {
return (
<TouchableOpacity onLongPress={() => props.longPressHandler(props.index)} >
.....
</TouchableOpacity>
)
}
Note: You have to pass an index from renderItem to ListItem and also from ListItem to longPressHandler function

How can I delete components one by one? [duplicate]

This question already has answers here:
How to delete an item from state array?
(18 answers)
Closed 2 years ago.
I'm currently developing an application using React Native.
This trial app has two buttons (ADD and DELETE).
When I press the ADD Button, a new component appears. If I press the DELETE Button that the same component disappears.
My question is: why can't I delete components one by one in this code?
even though I select an index of components I want to delete, all components disappear when I press a DELETE button...
How can I resolve this problem?
Here is the code (Screen):
import React, { useState } from "react";
import { ScrollView } from "react-native";
import Items from "../components/Items";
export default function Screen() {
const items = {
lists: [""],
};
const [stateItems, setStateItems] = useState(items);
return (
<ScrollView>
<Items stateItems={stateItems} setStateItems={setStateItems} />
</ScrollView>
);
}
Here is the code (Components):
import React from "react";
import { StyleSheet, View, Text, TouchableOpacity } from "react-native";
function Item({ index, handleAdd, handleDelete }) {
return (
<View style={styles.list}>
<TouchableOpacity
onPress={() => {
handleDelete(index);
}}
style={styles.buttonDelate}
>
<Text>DELETE</Text>
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
handleAdd();
}}
style={[styles.buttonDelate, styles.buttonAdd]}
>
<Text>ADD</Text>
</TouchableOpacity>
</View>
);
}
export default function Items(props) {
const stateItems = props.stateItems;
const setStateItems = props.setStateItems;
const handleAdd = () => {
setStateItems({
...stateItems,
lists: [...stateItems.lists, ""],
});
};
const handleDelete = (index) => {
const ret = stateItems.lists;
setStateItems({ ...ret, lists: [...ret].splice(index, 1) });
};
return (
<View>
{stateItems.lists.map((value, i) => (
<Item
key={value + i.toString()}
index={i}
stateItems={stateItems}
setStateItems={setStateItems}
handleAdd={handleAdd}
handleDelete={handleDelete}
/>
))}
</View>
);
}
const styles = StyleSheet.create({
list: {
width: "100%",
backgroundColor: "#ddd",
margin: 5,
padding: 10,
},
buttonDelate: {
backgroundColor: "tomato",
width: 80,
height: 40,
borderRadius: 5,
margin: 3,
justifyContent: "center",
alignItems: "center",
alignSelf: "center",
},
buttonAdd: {
backgroundColor: "orange",
},
});
node : 12.18.3
react native : 4.10.1
expo : 3.22.3
Splice returns the deleted items, not the original array. So when you do:
setStateItems({ ...ret, lists: [...ret].splice(index, 1) });
you're setting lists to the items deleted from [...ret], not the remaining ones.
We might also want to address why you're spreading the list into state and also setting lists to the deleted items from a copy of the original ret array.
const handleDelete = (index) => {
const ret = stateItems.lists;
setStateItems({ ...ret, lists: [...ret].splice(index, 1) });
};
The first part of this is stomping your other state with the lists array, which I highly doubt is what you want. Maybe you intended to preserve other state properties? Something more like:
const handleDelete = (index) => {
const { lists: old, ...others } = stateItems;
const lists = [...old];
lists.splice(index, 1);
setStateItems({ ...others, lists });
};

How to access a single element from Touchable Opacity inside a map function?

I am running an map function on my array which returns JSX in which I have a touchable opacity and inside that some text. So that touchable opacity is applied to each element of the array.
array.map((item, index) => {
<TouchableOpacity onPress={someFunction} >
<View>
<Text>{item.data}</Text>
</View>
</TouchableOpacity>
)}
Consider I have 4 array elements, I want to click on one and change the background color of only one (the selected) or the selected plus another touchableopacity. How can I achieve this?
You have to create a ref for each element and then set the style on click. Here is a working demo on snack : Dynamic ref with functional component
I worked with a functional compoennt, but if you are using a class, here a link to show you how to implements it : Dynamic ref with Class component
And in case Snack doesn't works, here is the code :
import * as React from 'react';
import { Text, View, StyleSheet, TouchableOpacity } from 'react-native';
export default function App() {
const myRefs = React.useRef([]);
const items = [
{
id:0,
name:"Item1"
},
{
id:1,
name:"Item2"
},
{
id:2,
name:"Item3"
}
];
const buildView = () => {
return items.map(item =>{
return(
<TouchableOpacity onPress={() => highlight(item.id)}>
<View ref={el => myRefs.current[item.id] = el}>
<Text>{item.name}</Text>
</View>
</TouchableOpacity>
)
});
}
const highlight = (itemId) => {
myRefs.current[itemId].setNativeProps({style: {backgroundColor:'#FF0000'}});
}
const resetColors = () => {
myRefs.current.forEach(ref =>
ref.setNativeProps({style:{backgroundColor:'transparent'}})
);
}
return (
<View>
{buildView()}
<Button title="Next question" onPress={resetColors} />
</View>
);
}
I create a ref fow each view and onPress, I just change its style. Do whatever you want in the highlight method.

Categories

Resources