React Native: How do I rerender only one specific flatlist item when changing its background color or other prop using context API and useReducer?

huangapple go评论78阅读模式
英文:

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&#39;s a [demo][3]


So first set up atoms. 
```js
import { atom } from &quot;jotai&quot;;
import { splitAtom } from &quot;jotai/utils&quot;;
import questions from &quot;./questions&quot;;

// 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 &quot;react-native&quot;;
import { useContext, useState } from &quot;react&quot;;
import { useAtom } from &quot;jotai&quot;;
import { splitAtom } from &#39;jotai/utils&#39;
import { questionsAtom } from &quot;./atoms&quot;;
import { COLORS } from &quot;./Theme&quot;;
import Question from &quot;./Question&quot;;

const { height } = Dimensions.get(&quot;window&quot;);
// 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 (
    &lt;View style={styles.container}&gt;
      &lt;FlatList
        data={questionsAtoms}
        horizontal
        pagingEnabled
        keyExtractor={(item) =&gt; item.id}
        // showsHorizontalScrollIndicator={false}
        renderItem={({ item, index }) =&gt; (
          &lt;Question
            questionAtom={item}
            onPress={() =&gt; console.log(&quot;hello&quot;)}
            num={index + 1}
          /&gt;
        )}
      /&gt;
      {isActive &amp;&amp; (
        &lt;View style={styles.bottomSheet}&gt;
          {qState.questions.map((q, ind) =&gt; {
            return (
              &lt;View
                key={ind}
                style={q.selected !== -1 ? styles.green : styles.gray}
              &gt;
                &lt;Text&gt;{q.id}&lt;/Text&gt;
              &lt;/View&gt;
            );
          })}
        &lt;/View&gt;
      )}
      &lt;TouchableOpacity
        activeOpacity={0.8}
        onPress={() =&gt; setIsActive(!isActive)}
        style={styles.toggle}
      &gt;&lt;/TouchableOpacity&gt;
     
    &lt;/View&gt;
  );
}

export default HomeScreen;

const styles = StyleSheet.create({
  container: {
    width: &quot;100%&quot;
  },
  bottomSheet: {
    width: &quot;100%&quot;,
    height: height * 0.9,
    backgroundColor: COLORS.lightWhite,
    position: &quot;absolute&quot;,
    bottom: 0,
    borderTopLeftRadius: 32,
    borderTopRightRadius: 32,
    elevation: 1,
    flexDirection: &quot;row&quot;,
    justifyContent: &quot;flex-start&quot;,
    alignItems: &quot;center&quot;,
    gap: 10,
    paddingHorizontal: 20,
    paddingTop: 30,
    flexWrap: &quot;wrap&quot;
  },
  toggle: {
    width: 60,
    height: 60,
    position: &quot;absolute&quot;,
    bottom: 20,
    right: 20,
    backgroundColor: &quot;#39e600&quot;,
    justifyContent: &quot;center&quot;,
    alignItems: &quot;center&quot;,
    borderRadius: 30,
    zIndex: 99,
    elevation: 1
  },
  toggleText: {
    padding: 10,
    borderRadius: 20,
    color: &quot;white&quot;
  },
  green: {
    backgroundColor: &quot;#33CC00&quot;,
    width: 40,
    height: 40,
    borderRadius: 20,
    alignItems: &quot;center&quot;,
    justifyContent: &quot;center&quot;
  },
  gray: {
    backgroundColor: COLORS.gray2,
    width: 40,
    height: 40,
    borderRadius: 20,
    alignItems: &quot;center&quot;,
    justifyContent: &quot;center&quot;
  }
});

Now useAtom to get/update the question:

import { useState } from &quot;react&quot;;
import { useAtom } from &quot;jotai&quot;;
import {
  Dimensions,
  FlatList,
  StyleSheet,
  Text,
  TouchableOpacity,
  View
} from &quot;react-native&quot;;

import { COLORS, SIZES } from &quot;./Theme&quot;;

const { width, height } = Dimensions.get(&quot;window&quot;);
const OPTON_TITLES = [&quot;A&quot;, &quot;B&quot;, &quot;C&quot;, &quot;D&quot;, &quot;E&quot;];

const Question = ({ questionAtom, onPress, num }) =&gt; {
  console.log(&quot;------- question was rendered -----------&quot;);
  const [question, setQuestion] = useAtom(questionAtom);
  return (
    &lt;View style={styles.questionContainer}&gt;
      &lt;Text style={styles.question}&gt;{`(Q.${num} )  ${question.question}`}&lt;/Text&gt;
      &lt;FlatList
        data={question.options}
        renderItem={({ item, index }) =&gt; (
          &lt;TouchableOpacity
            style={
              question.selectedOption === index
                ? styles.selectedOption
                : styles.option
            }
            activeOpacity={0.95}
            onPress={() =&gt; {
              //which option was selected =&gt; index
              //  question.selected = index;
              //onPress(question);
              setQuestion((prev) =&gt; {
                return {
                  ...prev,
                  selectedOption: index
                };
              });
            }}
          &gt;
            &lt;View style={styles.optionIndex}&gt;
              {
                &lt;Text style={styles.optionIndexTitle}&gt;
                  {OPTON_TITLES[index]}
                &lt;/Text&gt;
              }
              &lt;Text&gt;{item}&lt;/Text&gt;
            &lt;/View&gt;
          &lt;/TouchableOpacity&gt;
        )}
      /&gt;
    &lt;/View&gt;
  );
};

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: &quot;95%&quot;,
    alignSelf: &quot;center&quot;,
    fontSize: SIZES.large,
    paddingVertical: 20,
    paddingHorizontal: 10,
    color: COLORS.black,
    elevation: 1,
    backgroundColor: COLORS.white
  },
  selectedOption: {
    marginVertical: 8,
    width: &quot;95%&quot;,
    alignSelf: &quot;center&quot;,
    fontSize: SIZES.large,
    paddingVertical: 20,
    paddingHorizontal: 10,
    color: COLORS.black,
    elevation: 1,
    backgroundColor: &quot;#e6ffe6&quot;
  },
  optionIndex: {
    flexDirection: &quot;row&quot;,
    gap: 12
  },
  optionIndexTitle: {
    width: 20,
    height: 20,
    borderWidth: 1,
    borderColor: COLORS.gray,
    color: COLORS.gray,
    borderRadius: 10,
    textAlign: &quot;center&quot;
  }
});

huangapple
  • 本文由 发表于 2023年6月1日 20:58:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76382141.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定