React state wrongly updated in nested components with CodeMirror 6 Editor

268 Views Asked by At

I have a few components nested inside a larger "controller" component.

The whole demo app is below. There's also a StackBlitz.

import React, { useState } from 'react';
import CodeEditor from './CodeEditor';
import './style.css';

const PostEditor = ({ value, onChange }) => {
  return (
    <>
      {value && (
        <>
          <CodeEditor value={value} onChange={value => onChange(value)} />
        </>
      )}
    </>
  );
};

const Wrapper = ({ post, updatePostProperty }) => {
  return (
    <>
      <h2>Selected post: {post && post.title}</h2>
      {post && post.title && (
        <PostEditor
          value={post.title}
          onChange={value => {
            console.log('update title->', value);
            updatePostProperty(post.id, 'title', value);
          }}
        />
      )}
      {post && post.subTitle && (
        <PostEditor
          value={post.subTitle}
          onChange={value => {
            console.log('update subTitle->', value);
            updatePostProperty(post.id, 'subTitle', value);
          }}
        />
      )}
    </>
  );
};

export default function App() {
  const [posts, setPosts] = useState([
    { id: 1, title: 'post no 1', subTitle: 'subtitle no 1' },
    { id: 2, title: 'post no 2', subTitle: 'subtitle no 2' },
    { id: 3, title: 'post no 3', subTitle: 'subtitle no 3' }
  ]);
  const [post, setPost] = useState();

  const updatePostProperty = (id, property, value) => {
    const newPosts = [...posts];
    const index = newPosts.findIndex(post => (post.id === id));
    newPosts[index] = {
      ...newPosts[index],
      [property]: value
    };
    setPosts(newPosts);
  };
  return (
    <div>
      <ul>
        {posts &&
          posts.length > 0 &&
          posts.map((post, index) => (
            <li
              style={{ cursor: 'pointer' }}
              onClick={() => {
                setPost(post);
              }}
            >
              {post.title} - {post.subTitle}
            </li>
          ))}
      </ul>

      <Wrapper post={post} updatePostProperty={updatePostProperty} />
    </div>
  );
}

The App component hosts the updatePostProperty that is passed on to the Wrapper component which uses it when the PostEditor component triggers onChange the CodeEditor which is a wrapper for CodeMirror.

The issue here is that after you click one of the posts and edit the title and then the subtitle, the title gets reverted to the initial value.

Scenario:
Click on the first post and try to edit the title. Add an ! to the title. You'll see the post on the list gets updated.
After you edit the subtitle by adding a character to it, you'll see the title gets reverted to the previous state (without the !) in the App component.

Why is react doing this "revert" update?

gif demonstrating bad react behaviour


Update:

New StackBlitz.

  • I made a few adjustments to the script to use useEffect before changing the original posts array.
  • I added a regular input element to see if the problem persists. It seems that the issue is fixed with regular inputs.

However, I'd love someone's input on why does the issue still persists with the way CodeMirror is wired up.

2

There are 2 best solutions below

1
On
  1. it needs to be newPosts instead of posts
  2. no need to destructuring
  3. you are using 1 = here newPosts.findIndex(post => (post.id = id)); there suppose to be 2 == like newPosts.findIndex(post => (post.id == id));

checkout this code

  const updatePostProperty = (id, property, value) => {
    const newPosts = [...posts]; 
    const index = newPosts.findIndex(post => (post.id == id)); 
    newPosts[index][property] = value
    setPosts(newPosts);
  };
3
On

Inside updatePostProperty you're updating the wrong object.

You're updating:

posts[index] = {
  ...newPosts[index],
  [property]: value
};

But you want to update newPosts instead, so you have to do:

newPosts[index] = {
  ...newPosts[index],
  [property]: value
};