reactjsreact-tsx

React derivated state not updated properly


I want to develop a tabs component in React. When I try to close an element and assign the active state to the previous or the next tab my setFiles method updates the files array properly but when the context is reloaded the derivated state openedFiles is not using the correct files state.

This is the code I have:

context-provider.tsx

const IdeEmulatorContextProvider: React.FC<{children: React.ReactNode}> = ({children}) => {
  const [files, setFiles] = useState<IdeFile[]>([]);
  const openedFiles = files.filter((element: IdeFile) => element.opened);
  
  const selectFileHandler = (event: any) => {
    // ...
  };

  const openFileInTab = (file: IdeFile) => {
    // ...
  };

  const closeTab = (name: string) => {
    setFiles((currentFiles: IdeFile[]) => {
      let indexToUpdate: number;

      const newFiles = currentFiles.map((element, index) => {
        if (name === element.name) {
          indexToUpdate = index;
          return {
            ...element,
            opened: false,
            active: false
          };
        } else {
          return element;
        }
      });

      if (indexToUpdate! >= 1) {
        newFiles[indexToUpdate!-1] = {
          ...newFiles[indexToUpdate!-1],
          active: true
        };
      } else {
        if (openedFiles.length > 1) {
          newFiles[indexToUpdate!+1] = {
            ...newFiles[indexToUpdate!+1],
            active: true
          };
        }
      }

      return [...newFiles];
    });
  };

  const selectTab = (tabIndex: number) => {
    setFiles((currentFiles: IdeFile[]) => {
      const newFiles = currentFiles.map((element, index) => {
        if (tabIndex === index) {
          return {
            ...element,
            active: true
          };
        } else {
          return {
            ...element,
            active: false
          };
        }
      });

      return [...newFiles];
    });
  };

  const ctxValue = {
    files,
    openedFiles,
    selectFileHandler,
    openFileInTab,
    closeTab,
    selectTab
  };

  return <IdeEmulatorContext.Provider value={ctxValue}>
    {children}
  </IdeEmulatorContext.Provider>
};

tabs-list.tsx

const TabsList = () => {
  const ctx = useContext(IdeEmulatorContext);

  const closeHandler = (name: string) => {
    ctx.closeTab(name)
  }

  return <nav className="nav-bar">
    <ol className="tabs">
      {ctx.openedFiles.map((element: IdeFile, index: number) => <li key={element.name} className={element.active ? 'active' : ''} onClick={() => ctx.selectTab(index)}>
        {element.name} {element.active ? <button className="btn-close" onClick={() => closeHandler(element.name)}>x</button> : null}
      </li>)}
    </ol>
  </nav>
};

Thanks a lot in advance!


Solution

  • The issue you are facing is because of the bubbling of the close click event. Let me break down what's happening:

    To prevent this, you need stop the event propagation. This is how you can achieve this:

    const closeHandler = (event: React.MouseEvent<HTMLElement>, name: string) => {
         event.stopPropagation();
         ctx.closeTab(name);
    }
    
    //....
    onClick={(e) => closeHandler(e, element.name)}
    

    Edit (Just an advice, not much related with the actual issue) : Another issue I noticed with your logic is that you are passing index as an identifier in the ctx.selectTab(index). This will give you unwanted bugs whenever the item in openedFiles will be changed. To prevent this, use something that will be actually unique, such as name, tabId.

    Hope this helps :)