0

I am still pretty new to react and typescript in general, so there might be some other issues I am not seeing. Most of the tutorials I am finding are for class based components instead of functional ones, making it more difficult.

I have a component that contains two checkboxes. When toggling the checkbox, I would also like to post this update to a url. Currently, the toggles are working and I am able to update the state accordingly. The issue is when attempting to post the update, the updated state is not set in the request, but rather the previous state.

Below is the main Document component. I think the issue is with the updateDocument function, since the state has not necessarily been set by setToggles when it is called. From what I have read, I need to use a callback, but I am unsure how I would implement 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) {
            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>
    );
};

I just want to be able to pass up-to date values from the "state" provided by useState to a function within a functional component.

I will also add the whole file for the sake of completeness. But basically it is just a wrapper component around an array of Documents:

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>
    );
};

UPDATE 2:

Below is the final working code, slightly simplified from the OP. Thanks to @Ibz for all the help!

Regarding the duplicate POST requests: I was using yarn start to run a development server when I was seeing this issue. After building with yarn build and serving the files with the actual server, the issue is no longer present. This answer on the axios issues page made me try this.

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}
        />
    );
};
2
  • ({...prevState, [name]: checked}) does name need to be in square braces? Commented Nov 11, 2020 at 1:57
  • Unfortunately, nothing is immediately jumping out at me as to why it's sending multiple post requests. Does it stop at 2 identical requests? Or does the number just keep increasing with every toggle? Commented Nov 11, 2020 at 22:24

1 Answer 1

1

With useState, there's no guarantee that the action is executed in order, as your component needs to re-render to have all the up to date state.

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

That means with this piece of code, your component renders, and you have some value in toggles. When you get to setToggles(...), react queues the update of the state for the next render, so when you get to updateDocument, it's being run with the previous value of toggles.

To get around this, we would usually use useEffect. This is a hook which runs some code whenever some other value changes. In your instance, you would want something like:

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

The second argument to useEffect is called the Dependency Array, and is a list of values that when changed, causes the function inside useEffect to run.

It can be a little tricky wraping your head around state at first, but I hope this helped. Any other questions, just leave a comment. You can find more information here: https://reactjs.org/docs/hooks-effect.html

Sign up to request clarification or add additional context in comments.

9 Comments

Would I use useEffect inside the Document component or inside the handleToggle function?
Inside the Document component. React hooks need to be called at the top level of a component, and typically useEffect methods are defined straight after all the useState calls. So in your case, on the line after const [toggles, setToggles] = useState
This creates a different problem per my understanding. Since useEffect is called when an item in the dependency array is updated, each Document will fire off a POST request (in updateDocument) when the view is rendered. I only want to run updateDocument if handleToggle is called.
Ahhhh I see. You could try passing a function into the setState: setToggles(prevState => {const newState = {...prevState, [name]: checked}; updateDocument('http://example.com/update', document.id, document.description, newState.checked, newState.expired); return newState;}). I can post as a separate answer to format it better if you like
Thank you so much for your help so far! This is almost working and is basically what I wanted. There is one issue though: When toggling the second toggle, regardless if it is checked or expired, updateDocument is run twice? I am not quite sure if this is from updateDocument exactly, but I can see two POST requests being made. When toggling only a single toggle, only one POST is made. I have edited the OP to include the updated Document component with your suggestion.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.