React Native 和 Reanimated 错误:将对象添加到数组时,渲染的钩子数量比之前渲染的数量多

问题描述 投票:0回答:1

我无法让用户动态地将对象添加到使用 useSharedValues() 渲染到屏幕的数组中

这是我的主屏幕,其中对象被添加到数组中

import { useRoute } from '@react-navigation/native';
import React, { useState } from 'react';
import { Pressable, View } from 'react-native';

import PlayerView from '../../components/PlayerView/PlayerView';
import deck from './deck';
import styles from './styles';

export default function GameScreen() {
  const names = route.params?.names;

  const [players, setPlayers] = useState(() => initPlayers());
  const [round, setRound] = useState(1);
  const [currentDeck, setCurrentDeck] = useState(deck);
  const [tlayout, setLayout] = useState([]);
  const [playerTurnCounter, setPlayerTurnCounter] = useState(0); // used to keep track of whose turn it is
  const [deckSelected, setDeckSelected] = useState(false);

  function initPlayers() {
    const players = [];
    for (let i = 0; i < names.length; i++) {
      players.push({ id: i, name: names[i].name, points: 0, hand: [], subHand: [[], [], [], []] });
    }
    return players;
  }

  function handleDeckPress() {
    if (deckSelected) {
      const rand = Math.floor(Math.random() * currentDeck.length);
      const card = currentDeck[rand];
      currentDeck.splice(rand, 1);
      setCurrentDeck(currentDeck);
      addDeckCardToPlayerHand(card);
      setDeckSelected(false);
    } else if (!deckSelected) {
      setDeckSelected(true);
    }
  }

  const addDeckCardToPlayerHand = (card) => {
    const test = players[playerTurnCounter].hand;
    const newArray = [...test, { card, id: 3 }];

    setPlayers(
      players.map((player) => {
        if (player.id === playerTurnCounter) {
          return { ...player, hand: newArray };
        }
        return player;
      }),
    );
  };

  return (
    <View>
      <Pressable onPress={() => handleDeckPress()} hitSlop={{ bottom: 10 }}>
        {currentDeck.map((card, index) => (
          <View
            onLayout={(event) => {
              const { x, y, width, height } = event.nativeEvent.layout;
            }}
          />
        ))}
      </Pressable>
      <View
        onLayout={(event) => {
          event.target.measure((x, y, width, height, pageX, pageY) => {
            const t = tlayout;
            t.push({ x, y, width, height, pageX, pageY });
            // console.log('subhand one: ', t);
            setLayout(t);
          });
        }}
        style={styles.subHandOne}
      />
      <PlayerView
        style={styles.playerView}
        players={players}
        setPlayers={setPlayers}
        playerTurnCounter={playerTurnCounter}
        layout={tlayout}
      />
    </View>
  );
}

包含要渲染的对象数组的“播放器”被传递给以下组件:

import React from 'react';
import { View } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

import Card from '../Card/Card';
import CardList from '../CardList/CardList';
import styles from './styles';

export default function PlayerView({ players, setPlayers, playerTurnCounter }) {
  const player = initPlayer();

  function initPlayer() {
    for (let i = 0; i < players.length; i++) {
      if (players[i].id === playerTurnCounter) {
        return players[i];
      }
    }
    return players[0];
  }

  if (player.hand.length > 0) {
    return (
      <GestureHandlerRootView style={styles.container}>
        <View style={styles.cards}>
          <CardList players={players} setPlayers={setPlayers} playerTurnCounter={playerTurnCounter}>
            {player.hand.map((item) => (
              <Card key={item.id} item={item.card} count={item.id} />
            ))}
          </CardList>
        </View>
      </GestureHandlerRootView>
    );
  }
  return (
    <GestureHandlerRootView style={styles.container}>
      <View style={styles.cards} />
    </GestureHandlerRootView>
  );
}


CardList(计算手牌数组的位置):

/* eslint-disable react-hooks/rules-of-hooks */
/* eslint-disable react/no-array-index-key */
/* eslint-disable react/prop-types */
import React, { useState } from 'react';
import { Dimensions, View } from 'react-native';
import { runOnJS, runOnUI, useSharedValue } from 'react-native-reanimated';

import SortableCard from '../SortableCard/SortableCard';
import styles from './styles';

const containerWidth = Dimensions.get('window').width * 2;

export default function CardList({ children, players, setPlayers, playerTurnCounter }) {
  const [ready, setReady] = useState(false);
  const offsets = children.map(() => ({
    order: useSharedValue(0),
    width: useSharedValue(0),
    height: useSharedValue(0),
    x: useSharedValue(0),
    y: useSharedValue(0),
    originalX: useSharedValue(0),
    originalY: useSharedValue(0),
  }));

  if (!ready) {
    return (
      <View style={styles.row}>
        {children.map((child, index) => {
          return (
            <View
              key={child.key}
              onLayout={({
                nativeEvent: {
                  layout: { x, y, width, height }, // these give the original layout positions of the cards (and their sizes) when loaded in
                },
              }) => {
                const offset = offsets[index];
                offset.order.value = -1;
                offset.width.value = width / children.length + 10;
                offset.height.value = height;
                offset.originalX.value = x;
                offset.originalY.value = y;
                runOnUI(() => {
                  'worklet';

                  if (offsets.filter((o) => o.order.value !== -1).length === 0) {
                    runOnJS(setReady)(true);
                  }
                })();
              }}>
              {child}
            </View>
          );
        })}
      </View>
    );
  }
  return (
    <View style={styles.container}>
      {children.map((child, index) => (
        <SortableCard
          key={child.key}
          offsets={offsets}
          index={index}
          players={players}
          setPlayers={setPlayers}
          containerWidth={containerWidth}
          playerTurnCounter={playerTurnCounter}>
          {child}
        </SortableCard>
      ))}
    </View>
  );
}

SortableCard 组件包含负责动画手势的代码。

我的问题是,当我尝试将一个对象添加到该数组(称为“hand”)到将“hand”中的对象渲染到屏幕的组件时,我收到错误:

Error: Rendered more hooks than during the previous render 
, 当我期待这个添加到数组中的新对象渲染到屏幕上时。

我知道这个错误是由于调用 addDeckCardToPlayerHand 回调时 CardList 中的 offsets 数组中的钩子数量发生变化,从而改变了渲染的钩子数量。所以我的问题是,如果一开始 useSharedValues 的总量未知,如何允许用户动态创建将渲染到屏幕上的对象?

javascript react-native react-hooks react-native-reanimated react-native-hermes
1个回答
0
投票

问题

代码中的问题是

useSharedValue
钩子在循环/回调中被调用,这破坏了React的Hooks规则。 (强调我的

不支持在任何环境中调用Hooks(以use开头的函数) 其他情况,例如:

  • 🔴 不要在条件或循环内调用 Hook。
  • 🔴 不要在条件
    return
    语句之后调用 Hooks。
  • 🔴 不要在事件处理程序中调用 Hooks。
  • 🔴不要在类组件中调用Hooks。
  • 🔴 不要在传递给
    useMemo
    useReducer
    useEffect
    的函数内调用 Hook。
  • 🔴不要在
    try
    /
    catch
    /
    finally
    块内调用Hooks。

解决方案

如果我正确理解您的代码,那么您似乎正在创建一种“加载骨架”或在设置

children
状态之前对
ready
进行一些预先测量。

基于

useSharedValue
钩子文档,您可以存储数组值。

参见初始值:(强调我的

您最初想要存储在共享值中的值。它可以是 任何 JavaScript 值,如

number
string
boolean
,还有数据 结构如
array
object

我建议使用带有“子项”数组的单个

useSharedValue
钩子,并且可以在
View
组件的
onLayout
回调处理程序中更新和访问。

示例:

export default function CardList({ children, players, setPlayers, playerTurnCounter }) {
  const [ready, setReady] = useState(false);

  const offsets = useSharedValue(children.map(() => ({
    order: 0,
    width: 0,
    height: 0,
    x: 0,
    y: 0,
    originalX: 0,
    originalY: 0,
  }));

  if (!ready) {
    return (
      <View style={styles.row}>
        {children.map((child, index) => {
          return (
            <View
              key={child.key}
              onLayout={({
                nativeEvent: { layout: { x, y, width, height } },
              }) => {
                const offset = offsets[index];
                offset.order.value = -1;
                offset.width.value = width / children.length + 10;
                offset.height.value = height;
                offset.originalX.value = x;
                offset.originalY.value = y;

                runOnUI(() => {
                  'worklet';

                  if (offsets.filter((o) => o.order.value !== -1).length === 0) {
                    runOnJS(setReady)(true);
                  }
                })();
              }}>
              {child}
            </View>
          );
        })}
      </View>
    );
  }
  return (
    <View style={styles.container}>
      {children.map((child, index) => (
        <SortableCard
          key={child.key}
          offsets={offsets}
          index={index}
          players={players}
          setPlayers={setPlayers}
          containerWidth={containerWidth}
          playerTurnCounter={playerTurnCounter}>
          {child}
        </SortableCard>
      ))}
    </View>
  );
}
© www.soinside.com 2019 - 2024. All rights reserved.