reactjs 更改不带参照的特定子零部件的状态

gudnpqoy  于 2023-03-22  发布在  React
关注(0)|答案(2)|浏览(100)

我正在用React实现某种类型的测试应用程序。我有以下组件层次结构:
ContainerForWords 1-〉N Word 1-〉N Letter。
一开始我试着用refs来做,这样我就可以根据用户的输入来改变单词的状态,但是这变成了一场噩梦,因为refs并不是设计用来这样使用的(它们不会触发重新渲染)。

// ========== GeneratedTextAreaContainer
export const GeneratedTextAreaContainer = (): JSX.Element => {
  const { isRestartScheduled, setRestartScheduledStatus } = useContext(RestartContext);
  const trainingConfiguration = useAppSelector((store) => store.data.training);
  const wordRefs = useRef<WordRef[]>([]);

  const queryResult = useQuery({
    queryKey: ['generatedText'],
    queryFn: async () => {
      const data = await trainingHttpClient.getGeneratedText({
        ...trainingConfiguration,
        languageId: trainingConfiguration.languageInfo.id
      });

      wordRefs.current = [];
      setRestartScheduledStatus(false);

      return data;
    },
    enabled: isRestartScheduled
  });

  const generatedText = queryResult.data?.value ?? [];

  const wordElements = generatedText.map((wordLetters, wordIndex) => (
    <Word
      key={`word_${wordIndex}`}
      letters={wordLetters}
      isActive={wordIndex === 0}
      ref={(ref: WordRef) => {
        wordRefs.current.push(ref);
      }}
    />
  ));

  return (
    <>
      {isRestartScheduled ? (
        <LoaderElement />
      ) : (
        <GeneratedTextAreaFragment words={wordElements} wordRefs={wordRefs.current} />
      )}
    </>
  );
};

// ========== GeneratedTextAreaFragment

interface Props {
  words: ReactNode;
  wordRefs: WordRef[];
}

interface PositionInfo {
  positionInWord: number;
  wordIndex: number;
}

export const GeneratedTextAreaFragment = (props: Props): JSX.Element => {
  const [rerenderTrigger, setRerenderTrigger] = useState(false);
  const [positionInfo, setPositionInfo] = useState<PositionInfo>({
    positionInWord: 0,
    wordIndex: 0
  });

  useEffect(() => {
    setRerenderTrigger(!rerenderTrigger);
  }, []);

  const activeWord = props.wordRefs.find((w) => w.isActive);
  console.log(props.wordRefs);

  const keyDownHandler = (event: KeyboardEvent<HTMLDivElement>): void => {
    const char = event.key;
    const currentLetter = activeWord?.letterRefs[positionInfo.positionInWord];
    if (currentLetter?.character === char) {
      currentLetter?.setStatus('correct');
    } else {
      currentLetter?.setStatus('incorrect');
    }

    if (
      activeWord?.letterRefs !== undefined &&
      positionInfo.positionInWord === activeWord?.letterRefs?.length - 1
    ) {
      const newActiveWord = props.wordRefs[positionInfo.wordIndex + 1];
      activeWord.setIsActive(false);
      newActiveWord.setIsActive(true);
    }

    setPositionInfo((oldPositionInfo) => {
      if (
        activeWord !== undefined &&
        oldPositionInfo.positionInWord === activeWord?.letterRefs.length
      ) {
        return { wordIndex: oldPositionInfo.wordIndex + 1, positionInWord: 0 };
      }

      return {
        wordIndex: oldPositionInfo.wordIndex,
        positionInWord: oldPositionInfo.positionInWord + 1
      };
    });
  };

  return (
    <FocusLock>
      <Box sx={styles.wordsContainer} tabIndex={0} onKeyDown={keyDownHandler}>
        {props.words}
      </Box>
    </FocusLock>
  );
};

// ========== Word

interface Props {
  isActive: boolean;
  letters: string[];
}

interface WordRef {
  isActive: boolean;
  setIsActive: (isActive: boolean) => void;
  letterRefs: LetterRef[];
}

const Word = forwardRef((props: Props, ref) => {
  const [rerenderTrigger, setRerenderTrigger] = useState(false);
  const [isActive, setIsActive] = useState(props.isActive);
  const letterRefs = useRef<LetterRef[]>([]);

  useEffect(() => {
    setRerenderTrigger(!rerenderTrigger);
  }, []);

  useImperativeHandle(
    ref,
    () => {
      return {
        isActive,
        setIsActive,
        letterRefs: letterRefs.current
      };
    },
    [isActive, letterRefs]
  );

  const letterElements = props.letters.map((character, characterIndex) => {
    return (
      <Letter
        key={`char_${characterIndex}`}
        character={character}
        ref={(ref: LetterRef) => letterRefs.current.push(ref)}
      />
    );
  });

  console.log(letterRefs.current);

  return <Box sx={styles.word}>{letterElements}</Box>;
});

Word.displayName = 'Word';

export { Word };
export type { WordRef };

// ========== Letter

type Status = 'initial' | 'correct' | 'incorrect';

interface Props {
  character: string;
}

interface LetterRef {
  character: string;
  setStatus: (status: Status) => void;
}

const Letter = forwardRef((props: Props, ref) => {
  const [status, setStatus] = useState<Status>('initial');

  useImperativeHandle(ref, () => {
    return {
      character: props.character,
      setStatus
    };
  });

  const getStylesBasedOnState = (): SxProps => {
    if (status === 'correct') {
      return styles.correct;
    }
    if (status === 'incorrect') {
      return styles.incorrect;
    }
    return styles.initial;
  };

  return <Typography sx={{ ...getStylesBasedOnState() }}>{props.character}</Typography>;
});

Letter.displayName = 'Letter';

export { Letter };
export type { Status, LetterRef };
zc0qhyus

zc0qhyus1#

提升状态

Old official documentation
Brand new official documentation
没有例子很难准确回答,但是如果你想在父对象中改变子对象的状态,这可能意味着父对象需要保存它的子对象的状态。

function ContainerForWords(){
  const [wordsStates, setWordsStates] = useState( /* initial states as an array */);

  const changeWordState(wordIndex, newState){
   setWordsStates(previousStates => {
    const newStates = [...previousStates]; // copy previous state as we shouldn't mutate it directly
    newStates[wordIndex] = newState;
    return newStates;
   })
  }   
    
  return wordsStates.maps(wordState => <Word state={wordState} />);
}

或者,如果顺序无关紧要,你可以使用Map/Object单词-〉state来代替数组。注意不要直接修改map/object,你总是需要先复制它,因为状态是不可变的。

kg7wmglp

kg7wmglp2#

推荐使用Redux或其他状态管理工具。
https://redux.js.org/

相关问题