Search code examples
javascriptreactjsredux-toolkit

Updating a state variable in Redux store causing unwanted re-render of the entire app


I'm trying to learn redux-toolkit with a project.

I'm using toEditPostId variable (initial value is null) inside the store to get the id of the post I want to edit, the app works fine when I set its value to an id string, but when I click the clear button in my form component which calls dispatch(setToEditPostId(null)), then the entire app re-renders and handleSubmit function is automatically called and that then adds a new empty post, which I don't want to happen.

I have added **************** near the snippets which are causing problem.

import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axiosInstance  from "../utils/axiosInstance";

export const fetchMemoryPosts = createAsyncThunk(
  'posts/fetchPosts',
  async (_, thunkApi) => {
    try {
      const response = await axiosInstance.get('/posts');
      return response.data.posts;
    } catch (error) {
      const errorMessage = error.response.data.match(/Error: (.*?)<br>/)[1];
      const errorCode = error.response.status;
      return thunkApi.rejectWithValue({ errorMessage, errorCode });
    }
  }
);

export const createMemoryPost = createAsyncThunk(
  'posts/createPosts',
  async(data, thunkApi) => {
    const tagsArr = data.tags.split(',').map((ele) => ele.trim());
    const preparedData = { ...data, tags: tagsArr };

    try {
      const response = await axiosInstance.post(
        '/posts/create',
        preparedData
      );
      return response.data.data;
    } catch (error) {
      const errorMessage = error.response.data.match(/Error: (.*?)<br>/)[1];
      const errorCode = error.response.status
      return thunkApi.rejectWithValue({ errorMessage, errorCode })
    }
  }
)

const initialState = {
  posts: [],
  status: 'idle',
  error: null,
  // The state variable to track the id of the post to edit *************
  toEditPostId: null,
}

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    // changing the value to an id and back to null here ******************
    setToEditPostId(state, action) {
      state.toEditPostId = action.payload;
    }
  },
  extraReducers(builder) {
    builder
      .addCase(fetchMemoryPosts.pending, (state, _) => {
        state.status = 'loading';
      })
      .addCase(fetchMemoryPosts.fulfilled, (state, action) => {
        state.status = 'success';
        state.posts = action.payload;
      })
      .addCase(fetchMemoryPosts.rejected, (state, action) => {
        state.status = 'failure';
        state.error = action.payload;
      })
      .addCase(createMemoryPost.fulfilled, (state, action) => {
        state.status = 'success';
        state.posts = state.posts.concat(action.payload);
      })
      .addCase(createMemoryPost.rejected, (state, action) => {
        state.status = 'failure';
        state.error = action.payload;
      })
  }
});

export const getAllPostsSelector = (state) => state.posts.posts;
export const getPostsErrorSelector = (state) => state.posts.error;
export const getPostsStatusSelector = (state) => state.posts.status;
export const getToEditPostIdSelector = (state) => state.posts.toEditPostId;

export const { setToEditPostId } = postsSlice.actions;

export default postsSlice.reducer;

Below is the Form component.

import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
  createMemoryPost,
  setToEditPostId,
  getToEditPostIdSelector,
  getAllPostsSelector
} from '../../features/postsSlice';

const Form = () => {
  const dispatch = useDispatch();

  const allPosts = useSelector(getAllPostsSelector);
  const postId = useSelector(getToEditPostIdSelector);

  let targetPost;
  if (postId) {
    targetPost = allPosts.filter((post) => post._id === postId);
  }

  console.log("pi->", postId)
  console.log("po->", targetPost);


  const initialPostDataState = {
    creator: '', title: '', message:'', tags:''
  }

  const [postData, setPostData] = useState(initialPostDataState);

  // Below function runs unnecessarily when the
  // dispatch(setToEditPostId(null)) is called
  // ********************************* 
  const handleSubmit = (e) => {
    console.log("it ran");
    e.preventDefault()
    dispatch(createMemoryPost(postData));

    setPostData(initialPostDataState);
  }

  const handleInput = (e) => {
    setPostData({ ...postData, [e.target.name]: e.target.value })
  }

  const clearForm = () => {
    dispatch(setToEditPostId(null))
    setPostData(initialPostDataState);
  }

  return (
    <main className='bg-transparent_bg w-full flex'>
      <form
        className='w-full text-center space-y-3 p-2 box-border' 
        onSubmit={handleSubmit}
      >
        <h3 className='font-extrabold'>
          {postId !== null? "" : "Create a memory"}
        </h3>
        <input 
          className='input'
          type="text" 
          placeholder='Creator' 
          name='creator'
          value={postData.creator}
          onChange={handleInput}
        />
        <input 
          className='input'
          type="text" 
          placeholder='Title' 
          name='title'
          value={postData.title}
          onChange={handleInput}
        />
        <textarea 
          className='input'
          placeholder='Message'
          name="message"
          cols="30" 
          rows="5"
          value={postData.message}
          onChange={handleInput}
        />
        <input 
          className='input'
          type="text" 
          placeholder='Tags (coma seperated)' 
          name='tags'
          value={postData.tags}
          onChange={handleInput}
        />

        <div className='flex justify-around py-1 box-border'>
          <button
            type='submit'
            className='bg-blue-400 w-24'
          >
            Submit
          </button>
          <button
            // clear button to call dispatch to set value to
            // null *********************
            onClick={() => dispatch(setToEditPostId(null))}
            className='bg-red-500 w-24'
          >
            Clear
          </button>
        </div>
      </form>
    </main>
  )
}

export default Form;

Solution

  • button elements have type="submit" by default if you do not explicitly specify the button type.

    Button type attribute:

    type

    The default behavior of the button. Possible values are:

    • submit: The button submits the form data to the server. This is the default if the attribute is not specified for buttons associated with a <form>, or if the attribute is an empty or invalid value.
    • reset: The button resets all the controls to their initial values, like <input type="reset">. (This behavior tends to annoy users.)
    • button: The button has no default behavior, and does nothing when pressed by default. It can have client-side scripts listen to the element's events, which are triggered when the events occur.

    The second button to "clear" the toEditPostId state back to null is submitting the form as well. Specify that the "clear" button is not a "submit" type button.

    <button
      type='submit'
      className='bg-blue-400 w-24'
    >
      Submit
    </button>
    <button
      type="button" // <-- non-submit type button
      onClick={() => dispatch(setToEditPostId(null))}
      className='bg-red-500 w-24'
    >
      Clear
    </button>