#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
запрос (inupdateDocument
) при отображении представления. Я хочу запускать только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
компонент с вашим предложением.