javascript - Kan ikke få synkronisert tilstand i React INK når jeg bruker «useStdin»-hooken med rawMode


Jeg bruker ink-pakken, og jeg bygger følgende komponent:

MultiSelect.tsx-fil:

import React, { useEffect, useState } from 'react';
import { useStdin } from 'ink';

import type { ISelectItem, ISelectItemSelection } from './interfaces/select-item';
import { ARROW_DOWN, ARROW_UP, ENTER, SPACE } from './constants/input';

import MultiSelectView from './MultiSelect.view';

interface IProps {
    readonly items: ISelectItem[];
    readonly onSubmit: (selectedItems: string[]) => void;
}

const MultiSelect: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
    const { stdin, setRawMode } = useStdin();

    const [itemsSelectionState, setItemsSelectionState] = useState<ISelectItemSelection[]>(
        props.items.map((item, index) => ({ ...item, selected: false, isHighlighted: index === 0 })),
    );

    const stdinInputHandler = (data: unknown) => {
        const rawData = String(data);

        if (rawData === ARROW_DOWN) {
            setItemsSelectionState((prev) => {
                const highlightedIndex = prev.findIndex((item) => item.isHighlighted);

                if (highlightedIndex === -1) {
                    return prev;
                }

                const clonedPrev = structuredClone(prev);

                clonedPrev[highlightedIndex]!.isHighlighted = false;

                if (highlightedIndex === prev.length - 1) {
                    clonedPrev[0]!.isHighlighted = true;
                } else {
                    clonedPrev[highlightedIndex + 1]!.isHighlighted = true;
                }

                return clonedPrev;
            });
        }

        if (rawData === ARROW_UP) {
            setItemsSelectionState((prev) => {
                const highlightedIndex = prev.findIndex((item) => item.isHighlighted);

                if (highlightedIndex === -1) {
                    return prev;
                }

                const clonedPrev = structuredClone(prev);

                clonedPrev[highlightedIndex]!.isHighlighted = false;

                if (highlightedIndex === 0) {
                    clonedPrev[prev.length - 1]!.isHighlighted = true;
                } else {
                    clonedPrev[highlightedIndex - 1]!.isHighlighted = true;
                }

                return clonedPrev;
            });
        }

        if (rawData === SPACE) {
            setItemsSelectionState((prev) => {
                const highlightedIndex = prev.findIndex((item) => item.isHighlighted);

                if (highlightedIndex === -1) {
                    return prev;
                }

                const clonedPrev = structuredClone(prev);

                clonedPrev[highlightedIndex]!.selected = !prev[highlightedIndex]!.selected;

                return clonedPrev;
            });
        }

        if (rawData === ENTER) {
            const selectedItemsValues = itemsSelectionState
                .filter((item) => item.selected)
                .map((item) => item.value);

            props.onSubmit(selectedItemsValues);
        }
    };

    useEffect(() => {
        setRawMode(true);
        stdin?.on('data', stdinInputHandler);

        return () => {
            stdin?.removeListener('data', stdinInputHandler);
            setRawMode(false);
        };
    }, []);

    return <MultiSelectView itemsSelection={itemsSelectionState} />;
};

export default MultiSelect;

MultiSelect.view.tsx-fil:

import { Box, Text } from 'ink';
import React from 'react';

import type { ISelectItemSelection } from './interfaces/select-item';

interface IProps {
    readonly itemsSelection: ISelectItemSelection[];
}

const MultiSelectView: React.FC<IProps> = (props: React.PropsWithChildren<IProps>) => {
    return (
        <Box display="flex" flexDirection="column">
            {props.itemsSelection.map((item) => (
                <Box key={item.value} display="flex" flexDirection="row">
                    <Text color="blue">{item.isHighlighted ? '❯' : ' '}</Text>
                    <Text color="magenta">
                        &nbsp;
                        {item.selected ? '◉' : '◯'}
                        &nbsp;
                    </Text>
                    <Text color="white" bold={item.selected}>
                        {item.label}
                    </Text>
                </Box>
            ))}
        </Box>
    );
};

export default MultiSelectView;

Så, når jeg bruker denne komponenten i koden min:

        const onSubmitItems = (items: string[]) => {
            console.log(items);
        };

        render(<MultiSelect items={items} onSubmit={onSubmitItems} />);

funksjonen render importeres fra ink og items er noe som [{value: 'x', label: 'x'}, {value:'y', label:'y'}]

Når jeg trykker på enter-tasten, blir onSubmitItems utløst, men den gir en tom liste, selv om jeg hadde valgt noen elementer...

For eksempel, i dette outputet:

Jeg plukket 3 elementer, men resultatet er fortsatt en tom liste. Og det virker som om tilstanden endres, så hvorfor får jeg ikke den oppdaterte tilstanden?

Svar #1

Du kan løse problemet ved å bruke useCallback:

const stdinInputHandler = useCallback((data: Buffer) => {
        const rawData = String(data);

        if (rawData === ARROW_DOWN) {
            setItemsSelectionState((prev) => {
                const highlightedIndex = prev.findIndex((item) => item.isHighlighted);

                if (highlightedIndex === -1) {
                    return prev;
                }

                const clonedPrev = structuredClone(prev);

                clonedPrev[highlightedIndex]!.isHighlighted = false;

                if (highlightedIndex === prev.length - 1) {
                    clonedPrev[0]!.isHighlighted = true;
                } else {
                    clonedPrev[highlightedIndex + 1]!.isHighlighted = true;
                }

                return clonedPrev;
            });
        }

        if (rawData === ARROW_UP) {
            setItemsSelectionState((prev) => {
                const highlightedIndex = prev.findIndex((item) => item.isHighlighted);

                if (highlightedIndex === -1) {
                    return prev;
                }

                const clonedPrev = structuredClone(prev);

                clonedPrev[highlightedIndex]!.isHighlighted = false;

                if (highlightedIndex === 0) {
                    clonedPrev[prev.length - 1]!.isHighlighted = true;
                } else {
                    clonedPrev[highlightedIndex - 1]!.isHighlighted = true;
                }

                return clonedPrev;
            });
        }

        if (rawData === SPACE) {
            setItemsSelectionState((prev) => {
                const highlightedIndex = prev.findIndex((item) => item.isHighlighted);

                if (highlightedIndex === -1) {
                    return prev;
                }

                const clonedPrev = structuredClone(prev);

                clonedPrev[highlightedIndex]!.selected = !prev[highlightedIndex]!.selected;

                return clonedPrev;
            });
        }

        if (rawData === ENTER) {
            const selectedItemsValues = itemsSelectionState
                .filter((item) => item.selected)
                .map((item) => item.value);

            props.onSubmit(selectedItemsValues);
        }
    }, [itemsSelectionState]);

Deretter, i din useEffect:

  useEffect(() => {
        setRawMode(true);
        stdin?.on('data', stdinInputHandler);

        return () => {
            stdin?.removeListener('data', stdinInputHandler);
            setRawMode(false);
        };
    }, [stdinInputHandler]);

nå vil useEffect registrere inputbehandleren på nytt med nye funksjoner, lokale variabler oppdatert med tilstanden

Relaterte artikler