Web

How to Fix Duplicate Post on Update in Redux Toolkit

Fix duplicate posts when updating with Redux Toolkit. Stop double dispatches, use a proper update reducer, and switch to createEntityAdapter for robust CRUD.

1 answer 1 view

Why does updating a post in Redux Toolkit create a duplicate entry instead of replacing the existing one?

I’m building a CRUD application using Redux Toolkit for managing posts. The create and read operations work fine, but the update operation results in the updated post appearing twice in the list instead of replacing the original.

Here’s the relevant code from my postSlice.js:

js
export const postSlice = createSlice({
  name: "post",
  initialState,
  reducers: {
    update: (state, action) => {
      const previousPosts = state.entries.filter(
        (entry) => entry.id !== action.payload.id
      );

      state.entries = [action.payload, ...previousPosts];
    },
  }
});

In EditPost.jsx:

jsx
const EditPost = () => {
  const postId = useParams().id;

  const posts = useSelector((state) => state.post.entries);
  const selectedPost = posts.find((post) => post.id === postId);
  return (
    <div>
      <h2>This is Edit Post</h2>
      <PostForm selectedPost={selectedPost} />
    </div>
  );
};

And in PostForm.jsx (handling both create and update):

jsx
const PostForm = ({ selectedPost }) => {
  const navigate = useNavigate();
  const [formData, setFormData] = useState({
    id: selectedPost?.id || "",
    name: selectedPost?.name || "",
    desc: selectedPost?.desc || "",
    author: selectedPost?.author || "",
  });

  const dispatch = useDispatch();

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

  const handleSubmit = (e) => {
    e.preventDefault();

    if (selectedPost) {
      const updatePost = {
        id: formData.id,
        name: formData.name,
        desc: formData.desc,
        author: formData.author,
      };
      dispatch(update(updatePost));
      navigate("/");
    }

    const newPost = {
      id: nanoid(),
      name: formData.name,
      desc: formData.desc,
      author: formData.author,
    };

    dispatch(create(newPost));

    setFormData({
      name: "",
      desc: "",
      author: "",
    });
  };

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="name"
        placeholder="Name"
        value={formData.name}
        onChange={handleChange}
      />
      <input
        type="text"
        name="desc"
        placeholder="Desc"
        value={formData.desc}
        onChange={handleChange}
      />
      <input
        type="text"
        name="author"
        placeholder="Author"
        value={formData.author}
        onChange={handleChange}
      />
      <button type="submit">{selectedPost ? "Update" : "Create"}</button>
    </form>
  );
};

export default PostForm;

After creating a post and then updating it, the updated version appears as a duplicate in the list. How can I fix the update reducer to properly replace the existing post without creating duplicates? Any suggestions for improving the Redux Toolkit slice for CRUD operations would be helpful.

Redux Toolkit update redux duplicates usually come from dispatching both an update and a create (or from id/type mismatches), so your updated post gets inserted instead of replacing the original. In your code the form always calls create(…) after update(…), which produces the duplicate; fix the component dispatch logic and make the slice update replace an existing item (use findIndex + assignment or normalize with the createEntityAdapter helpers).

Contents

Symptom — duplicate post after update (Redux Toolkit)

You create a post, then open an edit form and submit — the updated post appears twice in the list. Looking at your PostForm.handleSubmit, the create(…) dispatch is executed unconditionally (it sits after the update branch), so every submit creates a new post even while you also dispatch update(…). That’s the immediate cause of the duplicate.

Before changing reducer logic, check whether your UI is sending two actions. Open the Redux DevTools and look at which actions run when you hit Submit.

Why updates end up as duplicates

There are three common reasons this happens:

  • Component-level bug: you dispatch both update and create for the same submit (your code does exactly that).
  • ID/type mismatch: compare by strict equality (===) between a string route param and a numeric id will fail, so the app thinks the entity is “new” and creates another one.
  • Reducer pattern mistakes: with createSlice you must either mutate the draft state or return a new state object — reassigning the local state variable inside a reducer does nothing. The Redux Toolkit docs on Immer reducers explain the allowed patterns, and the Redux immutable update patterns guide shows the pitfalls.

There are also adapter-specific edge cases (for example, changing an entity’s id can replace/merge other entities); see the createEntityAdapter docs and this repo issue showing duplicate-id behavior when IDs collide: https://github.com/reduxjs/redux-toolkit/issues/2018.

Immediate fix — change the component dispatch logic

The fastest, correct fix is to stop dispatching create when you intend to update. Move the create(…) call into an else block (or return early after update). Updated handleSubmit:

jsx
const handleSubmit = (e) => {
  e.preventDefault();

  if (selectedPost) {
    const updatePost = {
      id: formData.id,
      name: formData.name,
      desc: formData.desc,
      author: formData.author,
    };
    dispatch(update(updatePost));
    navigate("/");
    return; // prevents the create(...) below from running
  }

  const newPost = {
    id: nanoid(),
    name: formData.name,
    desc: formData.desc,
    author: formData.author,
  };

  dispatch(create(newPost));

  setFormData({
    name: "",
    desc: "",
    author: "",
  });

  navigate("/");
};

Or use if/else:

jsx
if (selectedPost) {
  dispatch(update(updatePost));
} else {
  dispatch(create(newPost));
}
navigate('/');

After this change, the duplicate caused by a second create action will stop.

Fixing the Redux Toolkit slice reducer (safe replace patterns)

Your reducer currently filters out the old item then prepends the updated payload:

js
const previousPosts = state.entries.filter(
  (entry) => entry.id !== action.payload.id
);
state.entries = [action.payload, ...previousPosts];

That can work if ids match and if only update is dispatched, but it’s safer and more explicit to either:

  • Replace the item in place (preferred, O(1) mutation with Immer), or
  • Map over the array and swap the matching item.

Mutating draft example (handles type normalization):

js
update: (state, action) => {
  const id = String(action.payload.id);
  const index = state.entries.findIndex(e => String(e.id) === id);
  if (index !== -1) {
    // Replace the existing entry
    state.entries[index] = action.payload;
  } else {
    // If it doesn't exist, insert (or ignore, depending on your logic)
    state.entries.unshift(action.payload);
  }
}

Map-based replacement (pure-return style):

js
update: (state, action) => {
  const id = String(action.payload.id);
  return {
    ...state,
    entries: state.entries.map(e =>
      String(e.id) === id ? action.payload : e
    ),
  };
}

Important rules:

Also normalize id types (use String(…) when comparing) to avoid mismatches between route string ids and numeric ids stored in state.

Use createEntityAdapter for robust CRUD (Redux Toolkit)

For collections, the built-in adapter standardizes shape and gives you battle-tested reducers:

js
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';

const postsAdapter = createEntityAdapter();
const initialState = postsAdapter.getInitialState();

const postsSlice = createSlice({
  name: 'posts',
  initialState,
  reducers: {
    addPost: postsAdapter.addOne,
    // updateOne expects { id, changes } and merges changes into the entity
    updatePost: (state, action) =>
      postsAdapter.updateOne(state, { id: action.payload.id, changes: action.payload }),
    // upsertOne will add if missing or update if present (handy if you sometimes create client-side)
    upsertPost: (state, action) => postsAdapter.upsertOne(state, action.payload),
    removePost: postsAdapter.removeOne,
    setAllPosts: postsAdapter.setAll,
  },
});

export const { addPost, updatePost, upsertPost, removePost, setAllPosts } = postsSlice.actions;
export default postsSlice.reducer;

export const postsSelectors = postsAdapter.getSelectors(state => state.posts);

The adapter also gives selectors like selectAll, selectById, which reduce bugs introduced by manual array operations. The adapter docs are here: https://redux-toolkit.js.org/api/createEntityAdapter

Note: updateOne will silently ignore an update if there is no entity with the provided id; upsertOne is useful if you want to insert when missing.

ID typing, selectors and extra checks

  • Matching IDs: when you read route params via useParams(), you get strings. Either store IDs as strings (nanoid does that) or convert the param to number when appropriate: const postId = Number(useParams().id) or compare with String(entry.id) === postId.
  • Selected item lookup: use a selector or the adapter’s selectById to avoid repeating logic.
  • DevTools: when debugging duplicates, open Redux DevTools and inspect the actions — if you see both update and create dispatched, fix the UI logic first.

Tests and best practices to prevent duplicates

  • Single responsibility: a form should either create or update, not both. Use separate action flows or guard with if/else.
  • Normalize state: use createEntityAdapter for add/update/remove helpers and selectors.
  • Standardize ID type across client/server boundaries.
  • Unit test the reducer: test update replaces an entity, and create does not run simultaneously.
  • Log actions during development (or step through with DevTools) to catch accidental double-dispatches.
  • If your server returns the canonical saved object (with server id), prefer using that returned object to replace local state.

Sources

Conclusion

The duplicate appears because your form dispatches a create for every submit in addition to update; fix the component to only dispatch create when creating a new post, and make the slice update replace the matching entry (use findIndex + direct assignment, a map-based replace, or the createEntityAdapter helpers). Standardize id types and rely on adapter selectors to avoid this class of bugs in future.

Authors
Verified by moderation
Moderation
How to Fix Duplicate Post on Update in Redux Toolkit