Передача состояния в функции внутри функционального компонента React

#javascript #reactjs #typescript #semantic-ui-react

#javascript #reactjs #typescript #семантический-пользовательский интерфейс-реагировать

Вопрос:

Я все еще новичок в react и typescript в целом, поэтому могут быть некоторые другие проблемы, которые я не вижу. Большинство руководств, которые я нахожу, предназначены для компонентов на основе классов, а не для функциональных, что усложняет задачу.

У меня есть компонент, который содержит два флажка. При установке флажка я также хотел бы опубликовать это обновление в URL. В настоящее время переключатели работают, и я могу соответствующим образом обновить состояние. Проблема заключается в том, что при попытке опубликовать обновление в запросе задается не обновленное состояние, а предыдущее состояние.

Ниже приведен основной Document компонент. Я думаю, что проблема связана с updateDocument функцией, поскольку состояние не обязательно было задано setToggles при ее вызове. Из того, что я прочитал, мне нужно использовать обратный вызов, но я не уверен, как бы я это реализовал.

 const Document: FC<{ document: IDocument }> = ({document}): ReactElement => {
    const [toggles, setToggles] = useState<DocumentToggles>(_documentToggles)

    const updateDocument = (uri: string, id: string, desc: string, checked: boolean, expired: boolean) => {
        axios.post(uri, {
            id: id,
            description: desc,
            checked: checked,
            expired: expired
        }).then(response => {
            console.log(response.data)
        });
    }

    const handleToggle = (e: FormEvent<HTMLInputElement>, data: any) => {
        console.log(data)
        if (e.currentTarget !== null) {
            const {name, checked} = data;
            setToggles(prevState => ({...prevState, [name]: checked}))
            // THIS IS NOT WORKING
            updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
        }
    }

    const handleSubmit = (e: FormEvent) => {
        if (e.currentTarget !== null) {
            e.preventDefault()
            updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
        }
    }

    return (
        <Container>
            <Form onSubmit={handleSubmit}>
                <DocumentCheckboxes
                    checked={toggles.checked}
                    expired={toggles.expired}
                    handleToggle={handleToggle}
                />
                <Form.Field>
                    <Button fluid type="submit">
                        Save
                    </Button>
                </Form.Field>
            </Form>
        </Container>
    );
};
  

Я просто хочу иметь возможность передавать актуальные значения из «состояния», предоставляемого useState функцией, в функциональный компонент.

Я также добавлю весь файл для полноты картины. Но в основном это просто компонент-оболочка вокруг массива Document s:

 const _DOCS: IDocument[] = [{id: "666666666666", description: "TESTDESC", checked: false, expired: false}]

const MainAppView = () => {
    return (
        <div>
            <DocumentViewBox documents={_DOCS}/>
        </div>
    );
}

interface IDocument {
    id: string;
    description: string;
    checked: boolean;
    expired: boolean;
}

// DocumentViewBox is used to display a list of documents.
const DocumentViewBox: FC<{ documents: IDocument[] }> = ({documents}): ReactElement => {
  return (
        <div>
            {documents.map(doc => {
                return <Document key={doc.id} document={doc}/>
            })}
        </div>
    );
};

interface DocumentToggles {
    checked: boolean;
    expired: boolean;
}

const _documentToggles: DocumentToggles = {checked: false, expired: false}

const Document: FC<{ document: IDocument }> = ({document}): ReactElement => {
    const [toggles, setToggles] = useState<DocumentToggles>(_documentToggles)

    const updateDocument = (uri: string, id: string, desc: string, checked: boolean, expired: boolean) => {
        axios.post(uri, {
            id: id,
            description: desc,
            checked: checked,
            expired: expired
        }).then(response => {
            console.log(response.data)
        });
    }

    const handleToggle = (e: FormEvent<HTMLInputElement>, data: any) => {
        console.log(data)
        if (e.currentTarget !== null) {
            const {name, checked} = data;
            setToggles(prevState => ({...prevState, [name]: checked}))
            // THIS IS NOT WORKING
            updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
        }
    }

    const handleSubmit = (e: FormEvent) => {
        if (e.currentTarget !== null) {
            e.preventDefault()
            updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
        }
    }

    return (
        <Container>
            <Form onSubmit={handleSubmit}>
                <DocumentCheckboxes
                    checked={toggles.checked}
                    expired={toggles.expired}
                    handleToggle={handleToggle}
                />
                <Form.Field>
                    <Button fluid type="submit">
                        Save
                    </Button>
                </Form.Field>
            </Form>
        </Container>
    );
};

const DocumentCheckboxes: FC<{ checked: boolean, expired: boolean, handleToggle: (e: FormEvent<HTMLInputElement>, data: any) => void }> = ({checked, expired, handleToggle}): ReactElement => {
    return (
        <Container textAlign="left">
            <Divider hidden fitted/>
            <Checkbox
                toggle
                label="Checked"
                name="checked"
                onChange={handleToggle}
                checked={checked}
            />
            <Divider hidden/>
            <Checkbox
                toggle
                label="Expired"
                name="expired"
                onChange={handleToggle}
                checked={expired}
            />
            <Divider hidden fitted/>
        </Container>
    );
}
  

UPDATE:

Updated Document component with the change provided by @Ibz. The only issue now is that the POST request to the update url is run twice if multiple toggles are toggled. Toggling only a single component will not do this.

 const Document: FC<{ document: IDocument }> = ({document}): ReactElement => {
    const [toggles, setToggles] = useState<DocumentToggles>(_documentToggles)

    const updateDocument = (uri: string, id: string, desc: string, checked: boolean, expired: boolean) => {
        axios.post(uri, {
            id: id,
            description: desc,
            checked: checked,
            expired: expired
        }).then(response => {
            console.log(response.data)
        });
    }

    const handleToggle = (e: FormEvent<HTMLInputElement>, data: any) => {
        console.log(data)
        if (e.currentTarget !== null) {
            e.preventDefault()

            setToggles(prevState => {
                const newState = {...prevState, [data.name]: data.checked};
                updateDocument('http://example.com/update', document.id, document.description, newState.checked, newState.expired);
                return newState;
            })
        }
    }

    const handleSubmit = (e: FormEvent) => {
        if (e.currentTarget !== null) {
            e.preventDefault()
            updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
        }
    }

    return (
        <Container>
            <Form onSubmit={handleSubmit}>
                <DocumentCheckboxes
                    checked={toggles.checked}
                    expired={toggles.expired}
                    handleToggle={handleToggle}
                />
                <Form.Field>
                    <Button fluid type="submit">
                        Save
                    </Button>
                </Form.Field>
            </Form>
        </Container>
    );
};
  

ОБНОВЛЕНИЕ 2:

Ниже приведен окончательный рабочий код, слегка упрощенный по сравнению с OP. Спасибо @Ibz за всю помощь!

Что касается повторяющихся POST запросов: я использовал yarn start для запуска сервера разработки, когда увидел эту проблему. После сборки yarn build и обслуживания файлов на реальном сервере проблема больше не существует. Этот ответ на странице axios проблем заставил меня попробовать это.

 import React, {Component, useState, useEffect, FC, ReactElement, MouseEvent, FormEvent, ChangeEvent} from "react";
import {Container, Segment, Label, Checkbox, CheckboxProps} from "semantic-ui-react";
import axios from "axios";

interface IDocument {
    id: string;
    description: string;
    checked: boolean;
    expired: boolean;
}

const _DOCS: IDocument[] = [{id: '0', description: '', checked: false, expired: false}]

const MainAppView = () => {
    return (
        <div>
            <DocumentViewBox documents={_DOCS}/>
        </div>
    );
}

const DocumentViewBox: FC<{ documents: IDocument[] }> = ({documents}): ReactElement => {
    return (
        <div>
            {documents.map(doc => <Document key={doc.id} document={doc}/>)}
        </div>
    );
};

const defaultDocumentProps: IDocument = {id: '', description: '', checked: false, expired: false};

const Document: FC<{ document: IDocument }> = ({document}): ReactElement => {
    const [documentProps, setDocumentProps] = useState<IDocument>(defaultDocumentProps);

    // Run only once and set data from doc
    // as the initial state.
    useEffect(() => {
        setDocumentProps(document)
    }, []);

    const updateDocument = (uri: string, updateDoc: IDocument) => {
        axios.post(uri, updateDoc).then(response => {
            console.log('updateDocument response:')
            console.log(response.data)
        }).catch(err => {
            console.log('updateDocument error:'   err)
        });
    }

    const handleToggle = (e: FormEvent<HTMLInputElement>, data: CheckboxProps) => {
        e.preventDefault()
        setDocumentProps(prevState => {
            const {name, checked} = data;
            const newState = {...prevState, [name as string]: checked};
            console.log('handleToggle new state:')
            console.log(newState)
            updateDocument('http://example.com/update', newState);
            return newState;
        });
    }

    return (
        <Checkbox
            toggle
            label='Checked'
            name='checked'
            onChange={handleToggle}
            checked={documentProps.checked}
        />
    );
};
  

Комментарии:

1. ({...prevState, [name]: checked}) должно ли имя быть в квадратных скобках?

2. К сожалению, мне сразу не приходит в голову, почему он отправляет несколько запросов post. Останавливается ли он на 2 идентичных запросах? Или число просто продолжает увеличиваться с каждым переключением?

Ответ №1:

При использовании useState нет гарантии, что действие выполняется по порядку, так как ваш компонент нуждается в повторном рендеринге, чтобы иметь все актуальное состояние.

 setToggles(prevState => ({...prevState, [name]: checked}))
// THIS IS NOT WORKING
updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
  

Это означает, что с помощью этого фрагмента кода ваш компонент выполняет рендеринг, и у вас есть некоторое значение в переключениях. Когда вы переходите к setToggles(…) , react ставит в очередь обновление состояния для следующего рендеринга, поэтому, когда вы переходите к updateDocument , он запускается с предыдущим значением переключателей.

Чтобы обойти это, мы обычно используем useEffect . Это перехват, который запускает некоторый код всякий раз, когда изменяется какое-либо другое значение. В вашем случае вам нужно что-то вроде:

 useEffect(() => {
  updateDocument('http://example.com/update', document.id, document.description, toggles.checked, toggles.expired)
}, [document.id, document.description, toggles.checked, toggles.expired])
  

Второй аргумент useEffect называется Dependency Array , и представляет собой список значений, которые при изменении вызывают запуск функции внутри useEffect .

Сначала может быть немного сложно обернуть голову вокруг состояния, но я надеюсь, что это помогло. Любые другие вопросы, просто оставьте комментарий. Вы можете найти более подробную информацию здесь: https://reactjs.org/docs/hooks-effect.html

Комментарии:

1. Буду ли я использовать useEffect внутри Document компонента или внутри handleToggle функции?

2. Внутри компонента Document. Перехватчики React должны вызываться на верхнем уровне компонента, и обычно методы useEffect определяются сразу после всех вызовов useState. Итак, в вашем случае в строке после const [переключает, настраивает] = useState

3. Насколько я понимаю, это создает другую проблему. Поскольку useEffect вызывается при обновлении элемента в массиве зависимостей, каждый Document из них будет запускать POST запрос (in updateDocument ) при отображении представления. Я хочу запускать только updateDocument при handleToggle вызове.

4. Аааа, я понимаю. Вы могли бы попробовать передать функцию в setState: setToggles(prevState => {const newState = {...prevState, [name]: checked}; updateDocument('http://example.com/update', document.id, document.description, newState.checked, newState.expired); return newState;}) . Я могу опубликовать отдельный ответ, чтобы отформатировать его лучше, если хотите

5. Большое спасибо за вашу помощь до сих пор! Это почти работает и в основном то, что я хотел. Однако есть одна проблема: при переключении второго переключателя, независимо от того, выполняется ли он checked или expired , updateDocument выполняется дважды? Я не совсем уверен, точно ли это updateDocument , но я вижу, что выполняются два POST запроса. При переключении только одного переключателя выполняется только один POST . Я отредактировал OP, чтобы включить обновленный Document компонент с вашим предложением.