If you must have all of your state coupled together and you are willing to move away from vanilla react state management, jotai 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 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
So first set up atoms.
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"
}
});