Search code examples
javascriptreactjsreduxreact-redux

React-redux useSelector state not working in onSubmit function but working in other functions


Please can someone explain to me why the second implementation of my code is reading the status_code of the msg and my first implementation can't read the status_code of the msg?

First implementation

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import InputField from '../components/layout/InputField';
import { faShop, faGlobe, faHouse, faSign, faMapLocationDot, faCity, faEarthEurope, faUpload } from '@fortawesome/free-solid-svg-icons';
import Button from '../components/layout/Button';
import FormAlert from '../components/layout/FormAlert';
import { createOrUpdateStore, clearMessages } from '../reducers/storeSlice';

const CreateStore = () => {
    const [formData, setFormData ] = useState({
        name: '',
        shop_url: '',
        house: '',
        street: '',
        postalcode: '',
        city: '',
        country: ''
    });

    const dispatch = useDispatch();
    const { loading, store, msg, errors } = useSelector((state) => state.store);
    const navigate = useNavigate();
    
    const {
        name,
        shop_url,
        house,
        street,
        postalcode,
        city,
        country,
    } = formData;

    useEffect(() => {
        if (msg) {
            dispatch(clearMessages());
        }
    }, []);

    const onChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const getError = (name) => {
        const findError = errors.filter(error => error.param === name);
        if (findErfind errorh > 0) {
            const error = errors.find(error => error.param === name);
            return error;
        }
    }

    const onSubmit = (e) => {
        e.preventDefault();
        const result = dispatch(createOrUpdateStore(formData));
        if (result) {
            if (msg.status_code === '201') {
                navigate('/login');
            }
        }
    }

    return (
        <form onSubmit={(e) => onSubmit(e)} className='dashboard-form'>
            <span className='header-text'>Create your store</span>
            {JSON.stringify(msg) !== '{}' ? (<FormAlert alert={msg} />) : ''}
            <InputField type='text' label='Store name' name='name' value={name} changeHandler={onChange} error={getError('name')} icon={faShop} />
            <InputField type='text' label='Website address' name='shop_url' value={shop_url} changeHandler={onChange} error={getError('shop_url')} icon={faGlobe} />
            <InputField type='text' label='House' name='house' value={house} changeHandler={onChange} error={getError('house')} icon={faHouse} />
            <InputField type='text' label='Street' name='street' value={street} changeHandler={onChange} error={getError('street')} icon={faSign} />
            <InputField type='text' label='Postalcode' name='postalcode' value={postalcode} changeHandler={onChange} error={getError('postalcode')} icon={faMapLocationDot} />
            <InputField type='text' label='City' name='city' value={city} changeHandler={onChange} error={getError('city')} icon={faCity} />
            <InputField type='text' label='Country' name='country' value={country} changeHandler={onChange} error={getError('country')} icon={faEarthEurope} />
            <Button text='CREATE' loading={loading} icon={faUpload} />
        </form>
    )
}

export default CreateStore;

Second implementation

import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import PropTypes from 'prop-types';
import { useDispatch, useSelector } from 'react-redux';
import InputField from '../components/layout/InputField';
import { faShop, faGlobe, faHouse, faSign, faMapLocationDot, faCity, faEarthEurope, faUpload } from '@fortawesome/free-solid-svg-icons';
import Button from '../components/layout/Button';
import FormAlert from '../components/layout/FormAlert';
import { createOrUpdateStore, clearMessages } from '../reducers/storeSlice';

const CreateStore = () => {
    const [formData, setFormData ] = useState({
        name: '',
        shop_url: '',
        house: '',
        street: '',
        postalcode: '',
        city: '',
        country: ''
    });

    const dispatch = useDispatch();
    const { loading, store, msg, errors } = useSelector((state) => state.store);
    const navigate = useNavigate();
    
    const {
        name,
        shop_url,
        house,
        street,
        postalcode,
        city,
        country,
    } = formData;

    useEffect(() => {
        if (msg) {
            dispatch(clearMessages());
        }
    }, []);

    const onChange = (e) => {
        setFormData({ ...formData, [e.target.name]: e.target.value });
    };

    const getError = (name) => {
        const findError = errors.filter(error => error.param === name);
        if (findError.length > 0) {
            const error = errors.find(error => error.param === name);
            return error;
        }
    }

    const onSubmit = (e) => {
        e.preventDefault();
        dispatch(createOrUpdateStore(formData));
    }

    const redirectOnSuccess = (msg) => {
        if (msg.status_code === '201') {
            setTimeout(() => {
                dispatch(clearMessages());
            }, 2000);

            setTimeout(() => {
                dispatch(clearMessages());
                navigate('/store');
            }, 3000);
        }
    }

    redirectOnSuccess(msg);

    return (
        <form onSubmit={(e) => onSubmit(e)} className='dashboard-form'>
            <span className='header-text'>Create your store</span>
            {JSON.stringify(msg) !== '{}' ? (<FormAlert alert={msg} />) : ''}
            <InputField type='text' label='Store name' name='name' value={name} changeHandler={onChange} error={getError('name')} icon={faShop} />
            <InputField type='text' label='Website address' name='shop_url' value={shop_url} changeHandler={onChange} error={getError('shop_url')} icon={faGlobe} />
            <InputField type='text' label='House' name='house' value={house} changeHandler={onChange} error={getError('house')} icon={faHouse} />
            <InputField type='text' label='Street' name='street' value={street} changeHandler={onChange} error={getError('street')} icon={faSign} />
            <InputField type='text' label='Postalcode' name='postalcode' value={postalcode} changeHandler={onChange} error={getError('postalcode')} icon={faMapLocationDot} />
            <InputField type='text' label='City' name='city' value={city} changeHandler={onChange} error={getError('city')} icon={faCity} />
            <InputField type='text' label='Country' name='country' value={country} changeHandler={onChange} error={getError('country')} icon={faEarthEurope} />
            <Button text='CREATE' loading={loading} icon={faUpload} />
        </form>
    )
}

export default CreateStore;

I tried several methods for getting the msg in the onSubmit function but they didn't work. So I read the documentation of the react-redux toolkit and found out that you can use data gotten from the state with useSelector, and that you can use this data in any function but it still refused to work in the onSubmit() function. And this explains why the errors array was retrieved in the getError() function.


Solution

  • Issues

    If I had to hazard a guess it is that the createOrUpdateStore is an asynchronous action that should be awaited, and there's a stale closure over the selected msg state in the onSubmit callback. The second implementation skips both these issues by not waiting for any returned Promises from createOrUpdateStore to resolve and relying on the CreateStore component to rerender when the redux state updates.

    Solution to fix implementation 1

    Assuming this asynchronous createOrUpdateStore action returns a Promise it can be awaited. To overcome the stale msg closure the submit handler will need to manually access the store to get the current state.

    // declare submit handler async
    const onSubmit = async (e) => {
      e.preventDefault();
    
      // await asynchronous action to resolve with result
      const result = await dispatch(createOrUpdateStore(formData));
    
      if (result) {
        // have result, get msg from current state
        const state = store.getState();
        const { msg } = state.store;
      
        if (msg.status_code === '201') {
          navigate('/login');
        }
      }
    }
    

    Solution to fix implementation 2

    The redirectOnSuccess function is called unconditionally as an unintentional side-effect and issues other unintentional side-effect. To make these intentional side-effects the logic should be moved into a useEffect hook with appropriate dependency.

    const {
      loading,
      store,
      msg,
      errors
    } = useSelector((state) => state.store);
    const navigate = useNavigate();
    
    ...
    
    useEffect(() => {
      if (msg.status_code === '201') {
        dispatch(clearMessages());
        navigate('/store');
      }
    }, [msg, navigate]);