Search code examples
javascriptreactjsuse-effect

how to fix form submission with useEffect hook (as is: need to click submit twice)


App takes user options and creates an array objects randomly, and based on user options. (it's a gamer tag generator, writing to learn react.js). As is, App is a functional component and I use useState to store array of objects (gamertags) and the current selected options.

I use formik for my simple form. It takes two clicks to get a new item with updated options. I know why, options in state of App doesn't not update until it rerenders as the function for form submission is async. Therefore, all of my options are updated, after the first click, and are correct with the second because they were updated with the rerendering and after I needed them.

I know the solution is to use a useEffect hook, but despite reading over other posts and tuts, I don't understand how to apply it. It's my first instance of needing that hook and I'm still learning.

I wrote a simplified App to isolate the problem as much as possible and debug. https://codesandbox.io/s/morning-waterfall-impg3?file=/src/App.js

export default function App() {
    const [itemInventory, setItemInventory] = useState([
        { options: "apples", timeStamp: 123412 },
        { options: "oranges", timeStamp: 123413 }
    ]);

    const [options, setOptions] = useState("apples");

    const addItem = (item) => {
        setItemInventory([item, ...itemInventory]);
    };

    const createItem = () => {
        return { options: options, timeStamp: Date.now() };
    };

    class DisplayItem extends React.Component {
        render() { // redacted for brevity}

    const onFormUpdate = (values) => {
        const newOption = values.options;
        setOptions(newOption);
        addItem(createItem());
    };

    const UserForm = (props) => {
        return (
            <div>
                <Formik
                    initialValues={{
                        options: props.options
                    }}
                    onSubmit={async (values) => {
                        await new Promise((r) => setTimeout(r, 500));
                        console.log(values);
                        props.onUpdate(values);
                    }}
                >
                    {({ values }) => (
                        <Form> //redacted for brevity
                        </Form>
                    )}
                </Formik>
            </div>
        );
    };

    return (
        <div className="App">
            <div className="App-left">
                <UserForm options={options} onUpdate={onFormUpdate} />
            </div>
            <div className="App-right">
                {itemInventory.map((item) => (
                    <DisplayItem item={item} key={item.timeStamp} />
                ))}
            </div>
        </div>
    );
}

This is probably a "layup" for you all, can you help me dunk this one? Thx!

Solution

  • Solved problem by implementing the useEffect hook.

    Solution: The functions that create and add an item to the list, addItem(createItem()), become the first argument for the useEffect hook. The second argument is the option stored in state, [options]. The callback for the form, onFormUpdate only updates the option in state and no longer tries to alter state, i.e. create and add an item to the list. The useEffect 'triggers' the creation and addition of a new item, this time based on the updated option because the updated option is the second argument of the hook.

    Relevant new code:

        useEffect( () => {
            addItem(createItem());
        }, [options]);
    
        const onFormUpdate = (values) => {
            const newOption = values.options;
            setOptions(newOption);
            //addItem used to be here
        };