我正在用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 };
2条答案
按热度按时间zc0qhyus1#
提升状态
Old official documentation
Brand new official documentation
没有例子很难准确回答,但是如果你想在父对象中改变子对象的状态,这可能意味着父对象需要保存它的子对象的状态。
或者,如果顺序无关紧要,你可以使用Map/Object单词-〉state来代替数组。注意不要直接修改map/object,你总是需要先复制它,因为状态是不可变的。
kg7wmglp2#
推荐使用Redux或其他状态管理工具。
https://redux.js.org/