javascriptreactjsreduxredux-thunk

useSelector value is still null inside handleSubmit button in React


My code flow is that a user fills the form and submits it which goes to action calling the API and then it returns the postId and stores it in the reducer. Now my main React component has useSelector to get the latest state for the postId. So after calling the action and submitting the form I am doing the navigate to that post through '/classifieds/{postId}' but postId shows null inside the handleSubmit function whereas it shows the value of postId outside that function. So probably my way of thinking is wrong. Can someone help me in suggesting how should be the flow?

classifieds.js

import React, { useEffect, useState } from "react";
import { TextEditor } from "../../../components/Text-Editor/text-editor";
import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import InputGroup from 'react-bootstrap/InputGroup';
import { useDispatch, useSelector } from "react-redux";
import { getCategories, postClassifieds } from "../../../redux/actions/classifiedsAction";
import './post-classified.scss'
import { useNavigate } from "react-router-dom";

export const PostClassified = () => {
  const dispatch = useDispatch();
  const navigate = useNavigate();

  const classifiedsLoading = useSelector((state) => state.classifieds.loading);
  const postId = useSelector((state) => state.classifieds.postId);
  console.log("postID is 1",postId) //  this shows up with the id when handleSubmit is called
  
  const handleInputChange = (e) => {
    const { name, value, type, checked } = e.target;
    const inputValue = type === 'checkbox' ? checked : value;
    setFormData({ ...formData, [name]: inputValue });
  };

  const [formData, setFormData] = useState({
    heading: '',
    category:''
  });

  const [formSubmitMessage, setFormSubmitMessage] = useState(false);

  const newFormData = new FormData();

  const handleSubmit = async (e) => {
    e.preventDefault();
    setFormSubmitMessage(false);
      
    newFormData.append("heading",formData.heading);
    newFormData.append("category",formData.category);
    await dispatch(postClassifieds(newFormData));
    console.log("postID is 2",postId) //  this shows up null
    navigate(`/classifieds/${postId}`)
  };

  return (
    <div className="section properties">
      <div className="container">
        {classifiedsLoading
          ? (<></>)
          : <Form onSubmit={handleSubmit} encType="multipart/form-data" className="form-post">
            {/* form.............. */}
          </Form>
        }
      </div>
    </div>
  );
}

classifiedsAction.js

import axios from "axios";

export const POST_SUCCESS = "POST_SUCCESS";
export const POST_FAILURE = "POST_FAILURE";
export const CLASSIFIEDS_LOADING = "CLASSIFIEDS_LOADING";

// post classified
export const postClassifieds = (formData) => async(dispatch) => {
  try {
    const response = await axios.post('/posts', formData);
    dispatch({
      type: 'POST_SUCCESS',
      payload: response.data._id, // Assuming the server returns the new document's ID
    });
  } catch(error) {
    console.log("error is", error);
    dispatch({
      type: 'POST_FAILURE',
      payload: error.message,
    });
  }
};

// Classifieds loading
export const setClassifiedsLoading = () => {
  return {
    type: CLASSIFIEDS_LOADING,
  };
};

classifiedsReducer.js

import {
  CLASSIFIEDS_LOADING,
  POST_SUCCESS,
  POST_FAILURE
} from "../actions/classifiedsAction";
  
const initialState = {
  loading: true,
  postID: null,
  error: null
};
  
export const ClassifiedsReducer = (state = initialState, action) => {
  switch (action.type) {
    case POST_SUCCESS:
      return {
        ...state,
        postId: action.payload,
        loading: false,
      };
    case POST_FAILURE:
      return {
        ...state,
        error: action.payload,
        loading: false,
      };
    case CLASSIFIEDS_LOADING:
      return {
        ...state,
        loading: true,
      };
    default:
      return state;
  }
};

Solution

  • The issue here is that the submit handler has a stale closure over the selected postId state.

    Update the code such that your postClassifieds action returns a resolved value to the calling code that can be awaited.

    Example:

    export const postClassifieds = (formData) => async(dispatch) => {
      try {
        const { data } = await axios.post('/posts', formData);
    
        dispatch({
          type: 'POST_SUCCESS',
          payload: data._id,
        });
    
        return data._id; // <-- return postId to UI/calling code
      } catch(error) {
        console.log("error is", error);
    
        dispatch({
          type: 'POST_FAILURE',
          payload: error.message,
        });
    
        throw error; // <-- re-throw error for UI/calling code
      }
    };
    

    Wrap the asynchronous logic in a try/catch to handle the returned postId value or any thrown errors or Promise rejections.

    const handleSubmit = async (e) => {
      e.preventDefault();
    
      try {
        setFormSubmitMessage(false);
        const newFormData = new FormData();
        newFormData.append("heading", formData.heading);
        newFormData.append("category", formData.category);
    
        const postId = await dispatch(postClassifieds(newFormData));
    
        // Success if we get this far 😀
        console.log("postID is 2",postId);
        navigate(`/classifieds/${postId}`);
      } catch(error) {
        // handle/ignore fetch failures/etc
      }
    };