1

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.

Irfan Dev
  • 11
  • 2
  • I think this has to do with the fact that all of your state is bundled together. When you update `question.options` it recreate a new instance of reducer, causing all components that uses it to re-render. You might be able to use `React.memo` and `React.useMemo` to stop this, but I think a better approach would be remove that state from the reducer – PhantomSpooks Jun 01 '23 at 13:31
  • How?, I'm learning and new to react world, could you please make changes to that code, so that it doesn't do that unnecessary rerendering. – Irfan Dev Jun 01 '23 at 14:02
  • https://codesandbox.io/s/gifted-silence-ev78ey – PhantomSpooks Jun 01 '23 at 14:36
  • getting error, when I looked inside atom.js, it is empty – Irfan Dev Jun 01 '23 at 16:00
  • ah sry i normally use expo to code react-native on the web so I didnt know that you have to manually save each file – PhantomSpooks Jun 01 '23 at 16:01
  • I just fixed it i think https://codesandbox.io/s/gifted-silence-ev78ey – PhantomSpooks Jun 01 '23 at 16:03

1 Answers1

1

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"
  }
});
PhantomSpooks
  • 2,877
  • 2
  • 8
  • 13