Before you mark this question as a duplicate of a closure issue or a event binding issue, know that I have tried all of those and it does not work.
A working demo of the issue. Open the link. Run the project on android and check logs. If you click on any option in the first question, it will log the lastIndex (Here it is 4. Since there are a total of 5 questions) when it should log the firstIndex which is 0.
So I am using javascript map to loop over my questions array and return questions and their respective options. Inside I have another map to loop over options.
The checkbox is a react-native-elements component.
renderCard = (item, index) => {
return (
<Card style={styles.testCard}>
<View key={item.u_question_id}>
<Text style={styles.question}>{index + 1}. {item.question}</Text>
{item.options.map(({ option, checked }, i) => {
return (
<View key={i}>
<CheckBox
containerStyle={{ backgroundColor: "transparent", borderWidth: 0 }}
title={option}
checkedIcon='dot-circle-o'
uncheckedIcon='circle-o'
checked={checked}
onPress={() => this.onSelectOption(index, i)}
/>
</View>
)
})}
</View>
</Card>
)
}
What I am expecting is that onPress of a checkbox should send the questionIndex and the optionIndex into onSelectOption to change the state and view but onPress always sends the last index of the questions array so the options of the last question are getting changed and not the intended one.
My onSelectOption method. Here questionIndex is coming 4 if I have 5 questions even though I am pressing on the first question's options.
onSelectOption = (questionIndex, optionIndex) => {
const selectedLanguageQuestionsCopy = this.state.selectedLanguageQuestions;
selectedLanguageQuestionsCopy[questionIndex].options.forEach(option => {
option.checked = false;
});
selectedLanguageQuestionsCopy[questionIndex].options[optionIndex].checked = true;
this.setState({ assessmentData: selectedLanguageQuestionsCopy });
}
I have tried using:
onPress={() => this.onSelectOption(index, i)}
onPress={this.onSelectOption.bind(this, index, i)} and changing onSelectOption to a normal method instead of an array function.
But it does not work. I am always getting the last index of the questions array.
The place where I am calling the renderCard method. selectedLanguageQuestions is an array of objects.
<Swipe
data={selectedLanguageQuestions}
activeQuestion={activeQuestion}
renderCard={(item, i) => this.renderCard(item, i)}
onSwipeLeft={(ques, i) => this.setState({ activeQuestion: i })}
onSwipeRight={(ques, i) => this.setState({ activeQuestion: i })}
/>
Render method of Swipe:
render() {
return (
<View>
{this.renderCards()}
</View>
);
}
renderCards() {
return this.props.data.map((item, i) => {
if (i === this.state.index) {
return (
<Animated.View
key={i}
style={[this.getCardStyle(), styles.cardStyle]}
{...this.panResponder.panHandlers}
>
{this.props.renderCard(item, i)}
</Animated.View>
);
}
return (
<Animated.View
key={i}
style={[styles.cardStyle, { opacity: 0 }]}
{...this.panResponder.panHandlers}
>
{this.props.renderCard(item, i)}
</Animated.View>
)
});
}
It seems like on Swipe.js line 137
return (
<Animated.View
key={i}
style={[styles.cardStyle, { opacity: 0 }]}
{...this.panResponder.panHandlers}
>
{this.props.renderCard(item, i)}
</Animated.View>
)
You are keeping the answers above each others, so what's happening exactly is that you are clicking on the answers that have 0 opacity, but because you don't see them, you think the visible answers are those getting the event. So what I suggest is that you disable the events on the answers that have 0 opacity, like so:
return (
<Animated.View
key={i}
pointerEvents="none"
style={[styles.cardStyle, { opacity: 0 }]}
{...this.panResponder.panHandlers}
>
{this.props.renderCard(item, i)}
</Animated.View>
)
Just added pointerEvents="none" attribute; to see how the questions were interrupting your "press", set the opacity to something above 0.5 and you will see the problem.
I hope this solves your issue.
Related
I'm attempting to build a To-Do list feature for an application; however, I'm struggling to get it right.
Basically, I want a user to be able to create new list items via the "submit" key while typing, or a button (both already done). When the new list item is created, I want the corresponding TextInput within the FlatList to be automatically focused. This works...up to a certain point. Once I create 10 list items, the behavior stops working properly and the focus is no longer on the next, newly created FlatList item.
I've figured out that if I change the "initialNumToRender" property in the FlatList and increase it past 10, it will solve the problem, temporarily, until reaching that cap. However, I don't want to reduce the performance of my FlatList and I want to find a different solution. It seems to me that the entire FlatList is re-rendering past 10 items and the TextInput loses focus, but I haven't been able to grasp yet how to focus on the TextInputs created after the 10th row.
The FlatList in question:
<FlatList
key="flatlist"
ListHeaderComponent={listHeader()}
initialNumToRender={10}
data={listItems}
// TODO: Will need to change to use actual IDs at some point as opposed to just indexing
renderItem={({item, index}) => (
<>
<View style={listStyles.itemView}>
<Pressable onPress={() => radioButtonPressed(index)}>
<MainListRadioButton completed={getRadioButtonState(index)} />
</Pressable>
{listItems[index].text.length > 0 ? (
<TextInput
style={listStyles.itemText}
placeholder={`Item ${index + 1}`}
defaultValue={listItems[index].text}
value={listItems[index].text}
onChangeText={text => listItemTextChanged(text, index)}
ref={index === listItems.length - 1 ? lastListItemRef : null}
onSubmitEditing={() => addListItem()}
/>
) : (
// Text input for "empty" list items so that a backspace can delete the entire row.
<TextInput
style={listStyles.itemText}
placeholder={`Item ${index + 1}`}
defaultValue={listItems[index].text}
value={listItems[index].text}
onChangeText={text => listItemTextChanged(text, index)}
ref={index === listItems.length - 1 ? lastListItemRef : null}
onKeyPress={({nativeEvent}) => {
if (nativeEvent.key === 'Backspace') {
deleteListItem(index);
}
}}
/>
)}
</View>
<View style={styles.divider} />
</>
)}
/>
How I'm implementing the behavior to shift focus to the next row so far:
useEffect(() => {
if (lastListItemRef.current) {
lastListItemRef.current.focus();
}
}, [listItems]);
You'd probably be better off using scrollToIndex() along with useLayoutEffect since you're wanting to trigger the scroll on a visual change.
useLayoutEffect(() => {
flatlistRef.current?.scrollToIndex(listItems.length - 1)
}, [flatlistRef, listItems])
Rather than controlling your TextInput focus at the parent level, you could turn your listItem function into a ListItem component. By doing this, you can store the ref to each new ListItem component within each component instance, and on mount of each component, focus it.
So your DraggableFlatList declaration becomes something like this:
<DraggableFlatList
...
renderItem={({ item, index, drag, isActive }) => (
<ListItem
item={item}
index={index}
drag={drag}
isActive={isActive}
addListItem={addListItem}
deleteListItem={deleteListItem}
completed={getRadioButtonState(index)}
text={listItems[index].text}
onChange={listItemTextChanged}
radioButtonPressed={radioButtonPressed}
/>
)}
...
/>;
And your ListItem component definition looks something like this:
const ListItem = ({
item,
index,
drag,
isActive,
text,
completed,
onChange,
radioButtonPressed,
addListItem,
deleteListItem,
} = props) => {
// Create ref to this TextInput
const textRef = useRef(null);
// On mount, set the TextInput focus
useEffect(() => {
if (textRef.current) {
textRef.current.focus();
}
}, []);
return (
<>
<Pressable style={listStyles.itemView} onLongPress={drag}>
<Pressable onPress={() => radioButtonPressed(index)}>
<MainListRadioButton completed={completed} />
</Pressable>
<TextInput
ref={textRef}
style={listStyles.itemText}
placeholder={`Item ${index + 1}`}
defaultValue={text}
value={text}
onChangeText={(text) => onChange(text, index)}
onSubmitEditing={() => addListItem()}
onKeyPress={({ nativeEvent }) => {
if (nativeEvent.key === 'Backspace') {
deleteListItem(index);
}
}}
/>
</Pressable>
<View style={listStyles.divider} />
</>
);
};
I've updated your snack here as a POC: https://snack.expo.dev/TZk_48CHF
you need to know if listItems is not a empthy array, for that reason you need to create this function isFechet() to evaluate it.
useEffect(() => {
const goToFocus=await ()=>{
if (lastListItemRef.current) {
lastListItemRef.current.focus();
}
}
if(isFechet(listItems)){
goToFocus()
}
}, [listItems]);
Thanks everyone. I actually ended up figuring it out myself. The steps I took were the following:
Completely remove setRef and ref within the TextInput and component.
Add autoFocus=true as a property to the TextInput. Previously I had a typo without capitalizing the F.
Change onTextChange to onEndEditing so the List Items only update after submitting text as opposed to on each text change.
In this case you have to use the flatlist scrollToIndex function along with viewPosition property properly for the selected changes.Also you have to use the onScrollToIndexFailed prop in flatlist along with setTimeout.
viewPosition: 0.5 ---> put the selected item in middle,0 and 1 are other values
const const scrollToIndex = (ref, index = 0) => {
if (ref && ref.current && index > -1) {
ref.current.scrollToIndex({ index, viewPosition: 0.5 });
}
};
const flatListRef = useRef();
useEffect(() => {
const selectedIndex = getSelectedIndex();
setTimeout(() => {
scrollToIndex(flatListRef, selectedIndex);
}, 50);
}, [title, flatListRef]);
<FlatList
ref={flatListRef}
initialNumToRender={categoryFilters.length + 1}
onScrollToIndexFailed={info => {
if (flatListRef !== null && info.index > -1) {
setTimeout(
() =>
flatListRef.current.scrollToIndex({
index: info.index,
animated: true,
viewPosition: 0.5,
}),
50
);
}
}}
refreshing={false}
data={listItems}
listKey={(_, index) => index.toString()}
renderItem={()=>{}}
/>
Hi everyone I want when I click that the heart fills up and becomes red or it is empty and is black, only I can't achieve this result, neither the color nor the shape of the heart changes when I click on it. Can you help me?
function Discussion(props) {
const [heartTouched, setHeartTouched] = useState(new Array(DATA.length));
const NewArray = (id, value) => {
let array = heartTouched;
array[id] = value;
setHeartTouched(array);
console.log(heartTouched);
};
return (
<FlatList
style={{ backgroundColor: "#F3D26F" }}
data={DATA}
extraData={DATA}
keyExtractor={({ id }) => id.toString()}
renderItem={({ item, index }) => (
<Screen style={styles.container}>
<View style={styles.commentContainer}>
<View style={styles.textCommentContainer}>
<Text>{item.user}</Text>
<Text style={styles.comment}>Hello</Text>
</View>
<TouchableOpacity
style={styles.heartContainer}
onPress={
index === item.id - 1
? heartTouched[index]
? () => NewArray(index, false) + (item.like -= 1)
: () => NewArray(index, true) + (item.like += 1)
: null
}
>
<MaterialCommunityIcons
style={styles.heartStyle}
name={heartTouched[index] ? "heart" : "heart-outline"}
color={heartTouched[index] ? "red" : "black"}
size={24}
/>
</TouchableOpacity>
</View>
</Screen>
)}
/>
);
}
The problem mostly is from the line
let array = heartTouched;
When you need to change the state in react you have to do it in an immutable way,
meaning that you can't take the array and change one element, you have to recreate a new one, then change the new one.
let array = [...heartTouched];
React only trigger rerender when the state (if it was an array or object), is recreated, because it looks at the reference not the object, and when you change one element you are not changing the reference.
This way you are creating a new array from the state, hope this works for you.
I'm trying to create list with an altered style based on selection:
I have a dynamic state -
constructor(props) {
super(props);
this.state = {
inventoryArray: InvData.map(obj => ({...obj, clicked: false})),
}
}
I want to use the clicked state of each object to determine styling - but am not quite sure on th syntax to use - i've tried:
returnInventoryObjects = () => {
return (
this.state.inventoryArray.map((data, i) => {
return (
if({data.clicked}) ? {
<View key={i} style={[InventoryStyles.InventoryItem, InventoryStyles.InventoryItemSel ]}>
}
else{
<View key={i} style={[InventoryStyles.InventoryItem]}>
}
which doesn't work -
I've also tried an inline dynamic style ie -
returnInventoryObjects = () => {
return (
this.state.inventoryArray.map((data, i) => {
return (
<View key={i} style={[InventoryStyles.InventoryItem, {data.clicked} && InventoryStyles.InventoryItemSel ]}>
Can anyone please advise how to achieve this please?
Heres the error shown when implementing option 2 -
Styles used are:
InventoryItem:{
backgroundColor:'rgba(255,255,255,0.8)',
paddingVertical:8,
paddingHorizontal:8,
flexDirection:'row',
marginBottom:15,
},
InventoryItemSel:{
backgroundColor:'rgba(255,255,255,0.2)',
},
I think you tried to make conditional style like so:
<View
key={i}
style={
data.clicked
? [InventoryStyles.InventoryItem]
: [InventoryStyles.InventoryItem, InventoryStyles.InventoryItemSel]
}
/>
I would do:
// More maintanable
const DEFAULT = [InventoryStyles.InventoryItem];
<View
style={
data.clicked ? DEFAULT : [...DEFAULT, InventoryStyles.InventoryItemSel]
}
/>
Notice that the statement {data.clicked} is parsed as an expression within scope, and not as an object or something else that you meant.
So using it within an array or in if expression, is an syntax error.
{ // Scope
data.clicked // Expression
}
Please try this:
this.state.inventoryArray.map((data, i) => {
if(data.clicked) {
return (
<View key={i} style={[InventoryStyles.InventoryItem, InventoryStyles.InventoryItemSel ]} />
)
}
else {
return (
<View key={i} style={[InventoryStyles.InventoryItem]} />
)
}
}
React components that don't have children need to be self-closing (/ at the end). Notice the difference between
<View key={i} style={[InventoryStyles.InventoryItem, InventoryStyles.InventoryItemSel ]}>
and
<View key={i} style={[InventoryStyles.InventoryItem, InventoryStyles.InventoryItemSel ]} />
Be sure to correct this in all your View components
The rest looks fine!
I am trying to change a button background color when clicked and toggle between list of buttons within the same button list. I ahve been trying this since yesterday and stuck at point where i am not able to proceed further.
More details - I have a page with questions and multiple option for the question and use should only select either of the answer YES/No/NA and that button background should be changed to different color, it works for a single question but for multiple questions i am not understanding which unique value to use to toggle the button color with in the same question.
I have created a snack for it here is an URL - https://snack.expo.io/#jerrypatels/bossy-bagel for some reason the snack is not working as expected on web but works well with mobile view, so request you to try the mobile version.
Now if i click first question YES it changes the color of all questions to YES.
Image how it is working
How i need it to look like
The state checked is on your AlongsideScreen component, not on each individual set of buttons as you probably intended. You probably want to split up the list of questions, list of buttons and everything like that into their own separate components to make managing the state easier, and you might want to consider using class components. Also, when you run into a problem, try simplifying the problem to allow you to understand it easier. You have a lot of extra code in your example that isn't related to the problem that I'm just going to replace with ... for now.
By breaking it up, the error becomes obvious:
const AlongsideScreen = ({ navigation }) => {
const [along, setAlong] = useState();
const [checked, setChecked] = useState();
return (
// ... other view code here
<View style={styles.containerText}>
// We pass in our questions as a prop to QuestionsList.
<QuestionsList questions={Questions}> </QuestionsList>
</View>
);
};
const QuestionsList = (props) => {
return (
<FlatList
data={props.questions}
keyExtractor={(item, index) => '' + index}
renderItem={({ item }) => (
<ButtonSelect item={item}></ButtonSelect>
)}
/>
)
}
const ButtonSelect = ({ item }) => {
return (
<FlatList
data={item.data}
keyExtractor={(item, index) => '2' + index}
renderItem={({ item, index }) => (
<View style={{ padding: 10 }}>
<Text style={[styles.text, { padding: 10 }]}>
{item.Q}
</Text>
<View style={styles.btnP}>
{item.options.map((list, ind) => {
return (
<SelectableButton item={item} list={list}></SelectableButton>
);
})}
</View>
</View>
)}
/>
)
}
const SelectableButton = ({ item, list }) => {
return (
<TouchableOpacity
style={[
styles.btn2,
{
width: width / item.length - item.width,
backgroundColor:
checked === list.id // ERROR! checked is undefined
? "red"
: '#F4F6FC',
},
]}
onPress={() => {
setChecked(list.id); // ERROR! setChecked is undefined!
}}>
<Text
style={[
Style.normalText,
{ textAlign: 'center' },
]}>
{list.value}
</Text>
</TouchableOpacity>
)
}
This is just your code, but it's been rearranged and the parts irrelevant to the issue have been deleted.
To fix the problem that you see above, you should add a prop for a listener for presses to SelectableButton and a prop for whether the button is checked. On the ButtonSelect, you should have a prop for a listener for when the currently selected button changes. Pass in whether the button is checked and a listener to each SelectableButton, and when that listener is called, call the listener prop. You may need to keep repeating this chain up, as there should only be a single source of truth. This is the "React Way".
How do I get rid of this warning? I know I need to get rid of setState functions in the render method, but I need them, so where should I put them?
export default class List<T> extends React.PureComponent<ListProps<T>> {
state = { wrapped: false, iconName: "arrow-down" };
render(): React.Node {
const { rows, renderRow, title, onPress } = this.props;
if (this.state.wrapped === true) {
list = undefined;
this.setState({ iconName: "arrow-up" });
} else {
list = rows.map((row, index) => (
<View key={index} style={index !== rows.length - 1 ? styles.separator : {}}>
{renderRow(row, index)}
</View>
));
this.setState({ iconName: "arrow-down" });
}
return (
<TouchableWithoutFeedback>
<View style={styles.container}>
<View style={[styles.separator, styles.relative]}>
<Text style={styles.title}>{title}</Text>
<IconButton
style={styles.icon}
onPress={() => this.setState({ wrapped: !this.state.wrapped })}
name={this.state.iconName}
color="black"
/>
</View>
{list}
</View>
</TouchableWithoutFeedback>
);
}}
No, you don't need to get rid of setState calls in your render method in general. You just need to put them so that they are not called in each render call (by binding them to user events like clicks for example) and thereby trigger another re-render, that again calls setState and again re-renders and so on.
So in your particular case, you are firing setState right in the beginning in the if() { ... } else { ... } statements. No matter what this.state.wrapped is, you end up at setState.
Here is a possible solution for how you might want to change your code specifically to make it what I assume you want it to make:
export default class List<T> extends React.PureComponent<ListProps<T>> {
state = { wrapped: false };
render(): React.Node {
const { rows, renderRow, title, onPress } = this.props;
const { wrapped } = this.state;
return (
<TouchableWithoutFeedback>
<View style={styles.container}>
<View style={[styles.separator, styles.relative]}>
<Text style={styles.title}>{title}</Text>
<IconButton
style={styles.icon}
onPress={() => this.setState({ wrapped: !wrapped })}
name={wrapped ? "arrow-up" : "arrow-down"}
color="black"
/>
</View>
{!wrapped && (
<View key={index} style={index !== rows.length - 1 ? styles.separator : {}}>
{renderRow(row, index)}
</View>
)}
</View>
</TouchableWithoutFeedback>
);
}}
Because the value of your icon is directly correlated to wrapped, you don't need to specifically set the icon in the state. Rather infer it from wrapped.