React derivated state not updated properly

28 Views Asked by At

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!

1

There are 1 best solutions below

0
Chinz On BEST ANSWER

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

  • You click the tab element which updates the state and set the active flag to true
  • Now, you close the element by clicking on the X button. This action triggers two function: closeHandler and ctx.selectTab for the element. The trigger to ctx.selectTab is caused by the click on the button getting propagated to the parent (which is known as event bubbling).

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 :)