英文:
React Native: How do I rerender only one specific flatlist item when changing its background color or other prop using context API and useReducer?
问题
我需要一些帮助,我正在进行一个小型个人项目,这是一个问答应用程序,我有用于问题的JSON数据,我正在使用FlatList来渲染问答问题,每个列表项显示问题在顶部以及四个选项以选择正确答案。
当我点击任何选项时,我会将该选项的背景颜色更改为绿色,并将该特定问题的selected prop(默认为-1)更改为1,到目前为止,它运行得很完美。
我正在使用Context API和useReducer来管理状态。
我面临的问题是,当我点击任何选项时,整个FlatList都会重新渲染。我不希望如此,我只想重新渲染更改了prop的特定列表项。以下是sandbox链接:
https://codesandbox.io/s/elegant-wilson-c6o9fp
请查看控制台,我在每次重新渲染QuestionScreen时都进行了控制台记录。
英文:
I need some help, I'm working on a small personal project, it is a quiz app, I have json data for questions, I'm using flat list to render quiz questions and each list item shows question on top and four options to choose correct answer from.
When I tap on any option I am changing the background color to green of that particular option, also I'm changing selected prop (-1 default) to 1 of that particular question, so far it is working perfectly,
I'm using context api with useReducer to manage the state.
Problem I'm facing is when I tap on any option, the whole flatlist gets rerendered. I don't want that, I want only that particular listitem to be rerendered whose prop change. Here is the sandbox link:
https://codesandbox.io/s/elegant-wilson-c6o9fp
Look at the console , I'm console logging QuestionScreen each time it gets rerendered.
答案1
得分: 1
如果您必须将所有状态耦合在一起,并且愿意远离普通的 React 状态管理,jotai 可能是一个解决方案。
使用 React 上下文来管理状态有一个注意事项,即会导致意外的重新渲染(这是官方预期的行为);但是使用 jotai,您可以完全避免这种情况。此外,您可以使用 split 来修改数组中的元素,而无需重新创建数组;并且可以在只想更改一个项目时避免重新渲染整个列表。
这里有一个 演示。
首先设置原子(atoms)。
import { atom } from "jotai";
import { splitAtom } from "jotai/utils";
import questions from "./questions";
// 将所有问题作为单个原子(state)
export const questionsAtom = atom(questions);
您不需要为每个状态片段创建提供者。只需从您的原子文件导入原子,并使用 useAtom 钩子来获取/设置状态。请记住,使用 splitAtom
会将数组中的每个元素转换为其自己的原子:
import {
Dimensions,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import { useContext, useState } from "react";
import { useAtom } from "jotai";
import { splitAtom } from 'jotai/utils';
import { questionsAtom } from "./atoms";
import { COLORS } from "./Theme";
import Question from "./Question";
const { height } = Dimensions.get("window");
// splitAtom 允许您更新单个列表项而无需重新创建整个列表
const qAtoms = splitAtom(questionsAtom);
function HomeScreen() {
const [isActive, setIsActive] = useState(false);
const [questionsAtoms] = useAtom(qAtoms);
// 当 questionsAtoms 更新时,qState 仍会得到更新
const [qState, setQstate] = useAtom(questionsAtom);
return (
<View style={styles.container}>
<FlatList
data={questionsAtoms}
horizontal
pagingEnabled
keyExtractor={(item) => item.id}
// showsHorizontalScrollIndicator={false}
renderItem={({ item, index }) => (
<Question
questionAtom={item}
onPress={() => console.log("hello")}
num={index + 1}
/>
)}
/>
{isActive && (
<View style={styles.bottomSheet}>
{qState.questions.map((q, ind) => {
return (
<View
key={ind}
style={q.selected !== -1 ? styles.green : styles.gray}
>
<Text>{q.id}</Text>
</View>
);
})}
</View>
)}
<TouchableOpacity
activeOpacity={0.8}
onPress={() => setIsActive(!isActive)}
style={styles.toggle}
></TouchableOpacity>
</View>
);
}
export default HomeScreen;
const styles = StyleSheet.create({
container: {
width: "100%"
},
bottomSheet: {
width: "100%",
height: height * 0.9,
backgroundColor: COLORS.lightWhite,
position: "absolute",
bottom: 0,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
elevation: 1,
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
gap: 10,
paddingHorizontal: 20,
paddingTop: 30,
flexWrap: "wrap"
},
toggle: {
width: 60,
height: 60,
position: "absolute",
bottom: 20,
right: 20,
backgroundColor: "#39e600",
justifyContent: "center",
alignItems: "center",
borderRadius: 30,
zIndex: 99,
elevation: 1
},
toggleText: {
padding: 10,
borderRadius: 20,
color: "white"
},
green: {
backgroundColor: "#33CC00",
width: 40,
height: 40,
borderRadius: 20,
alignItems: "center",
justifyContent: "center"
},
gray: {
backgroundColor: COLORS.gray2,
width: 40,
height: 40,
borderRadius: 20,
alignItems: "center",
justifyContent: "center"
}
});
现在使用 useAtom
来获取/更新问题:
import { useState } from "react";
import { useAtom } from "jotai";
import {
Dimensions,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import { COLORS, SIZES } from "./Theme";
const { width, height } = Dimensions.get("window");
const OPTON_TITLES = ["A", "B", "C", "D", "E"];
const Question = ({ questionAtom, onPress, num }) => {
console.log("------- question was rendered -----------");
const [question, setQuestion] = useAtom(questionAtom);
return (
<View style={styles.questionContainer}>
<Text style={styles.question}>{`(Q.${num} ) ${question.question}`}</Text>
<FlatList
data={question.options}
renderItem={({ item, index }) => (
<TouchableOpacity
style={
question.selectedOption === index
? styles.selectedOption
: styles.option
}
activeOpacity={0.95}
onPress={() => {
setQuestion((prev) => {
return {
...prev,
selectedOption: index
};
});
}}
>
<View style={styles.optionIndex}>
{
<Text style={styles.optionIndexTitle}>
{OPTON_TITLES[index]}
</Text>
}
<Text>{item}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
);
};
export default Question;
const styles = StyleSheet.create({
questionContainer: {
width: width,
height: height,
paddingHorizontal: 5,
paddingVertical: 20
},
question: {
fontSize: SIZES.medium,
fontWeight: 500,
marginBottom: 16,
padding: 10,
color: COLORS.black
},
option: {
marginVertical: 8,
width: "95%",
alignSelf: "center",
fontSize: SIZES.large,
paddingVertical: 20,
paddingHorizontal: 10,
color: COLORS.black,
elevation: 1,
backgroundColor: COLORS.white
},
selectedOption: {
marginVertical: 8,
width: "95%",
alignSelf: "center",
fontSize: SIZES.large,
<details>
<summary>英文:</summary>
If you must have all of your state coupled together and you are willing to move away from vanilla react state management, [jotai][1] could be a solution.
Using react context for state management has a caveat of having unexpected re-renders (this is officially the expected behavior); but with jotai you can entirely skip this. Furthermore, you can use [split][2] to modify an element in an array without needing to re-creating the array; and can avoid re-rendering your entire list when you want to change just one item.
Here's a [demo][3]
So first set up atoms.
```js
import { atom } from "jotai";
import { splitAtom } from "jotai/utils";
import questions from "./questions";
// all of questions as a single atom(state)
export const questionsAtom = atom(questions);
You dont need to create providers for each piece of state. Just import
the atom from your atom file and use the useAtom hook to get/set state. Keep in mind that using splitAtom
will turn each element in your array into an atom of its own:
import {
Dimensions,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import { useContext, useState } from "react";
import { useAtom } from "jotai";
import { splitAtom } from 'jotai/utils'
import { questionsAtom } from "./atoms";
import { COLORS } from "./Theme";
import Question from "./Question";
const { height } = Dimensions.get("window");
// splitAtom will allow you update individual
// list items without recreating the whole list
const qAtoms = splitAtom(questionsAtom);
function HomeScreen() {
const [isActive, setIsActive] = useState(false);
const [questionsAtoms] = useAtom(qAtoms);
// qState will still get updated when questionsAtoms update
const [ qState,setQstate] = useAtom(questionsAtom)
return (
<View style={styles.container}>
<FlatList
data={questionsAtoms}
horizontal
pagingEnabled
keyExtractor={(item) => item.id}
// showsHorizontalScrollIndicator={false}
renderItem={({ item, index }) => (
<Question
questionAtom={item}
onPress={() => console.log("hello")}
num={index + 1}
/>
)}
/>
{isActive && (
<View style={styles.bottomSheet}>
{qState.questions.map((q, ind) => {
return (
<View
key={ind}
style={q.selected !== -1 ? styles.green : styles.gray}
>
<Text>{q.id}</Text>
</View>
);
})}
</View>
)}
<TouchableOpacity
activeOpacity={0.8}
onPress={() => setIsActive(!isActive)}
style={styles.toggle}
></TouchableOpacity>
</View>
);
}
export default HomeScreen;
const styles = StyleSheet.create({
container: {
width: "100%"
},
bottomSheet: {
width: "100%",
height: height * 0.9,
backgroundColor: COLORS.lightWhite,
position: "absolute",
bottom: 0,
borderTopLeftRadius: 32,
borderTopRightRadius: 32,
elevation: 1,
flexDirection: "row",
justifyContent: "flex-start",
alignItems: "center",
gap: 10,
paddingHorizontal: 20,
paddingTop: 30,
flexWrap: "wrap"
},
toggle: {
width: 60,
height: 60,
position: "absolute",
bottom: 20,
right: 20,
backgroundColor: "#39e600",
justifyContent: "center",
alignItems: "center",
borderRadius: 30,
zIndex: 99,
elevation: 1
},
toggleText: {
padding: 10,
borderRadius: 20,
color: "white"
},
green: {
backgroundColor: "#33CC00",
width: 40,
height: 40,
borderRadius: 20,
alignItems: "center",
justifyContent: "center"
},
gray: {
backgroundColor: COLORS.gray2,
width: 40,
height: 40,
borderRadius: 20,
alignItems: "center",
justifyContent: "center"
}
});
Now useAtom
to get/update the question:
import { useState } from "react";
import { useAtom } from "jotai";
import {
Dimensions,
FlatList,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import { COLORS, SIZES } from "./Theme";
const { width, height } = Dimensions.get("window");
const OPTON_TITLES = ["A", "B", "C", "D", "E"];
const Question = ({ questionAtom, onPress, num }) => {
console.log("------- question was rendered -----------");
const [question, setQuestion] = useAtom(questionAtom);
return (
<View style={styles.questionContainer}>
<Text style={styles.question}>{`(Q.${num} ) ${question.question}`}</Text>
<FlatList
data={question.options}
renderItem={({ item, index }) => (
<TouchableOpacity
style={
question.selectedOption === index
? styles.selectedOption
: styles.option
}
activeOpacity={0.95}
onPress={() => {
//which option was selected => index
// question.selected = index;
//onPress(question);
setQuestion((prev) => {
return {
...prev,
selectedOption: index
};
});
}}
>
<View style={styles.optionIndex}>
{
<Text style={styles.optionIndexTitle}>
{OPTON_TITLES[index]}
</Text>
}
<Text>{item}</Text>
</View>
</TouchableOpacity>
)}
/>
</View>
);
};
export default Question;
const styles = StyleSheet.create({
questionContainer: {
width: width,
height: height,
paddingHorizontal: 5,
paddingVertical: 20
},
question: {
fontSize: SIZES.medium,
fontWeight: 500,
marginBottom: 16,
padding: 10,
color: COLORS.black
},
option: {
marginVertical: 8,
width: "95%",
alignSelf: "center",
fontSize: SIZES.large,
paddingVertical: 20,
paddingHorizontal: 10,
color: COLORS.black,
elevation: 1,
backgroundColor: COLORS.white
},
selectedOption: {
marginVertical: 8,
width: "95%",
alignSelf: "center",
fontSize: SIZES.large,
paddingVertical: 20,
paddingHorizontal: 10,
color: COLORS.black,
elevation: 1,
backgroundColor: "#e6ffe6"
},
optionIndex: {
flexDirection: "row",
gap: 12
},
optionIndexTitle: {
width: 20,
height: 20,
borderWidth: 1,
borderColor: COLORS.gray,
color: COLORS.gray,
borderRadius: 10,
textAlign: "center"
}
});
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论