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!
The issue you are facing is because of the bubbling of the close click event. Let me break down what's happening:
tab elementwhich updates the state and set theactiveflag to trueclosethe element by clicking on the X button. This action triggers two function:closeHandlerandctx.selectTabfor the element. The trigger toctx.selectTabis 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:
Edit (Just an advice, not much related with the actual issue) : Another issue I noticed with your logic is that you are passing
indexas an identifier in thectx.selectTab(index). This will give you unwanted bugs whenever the item inopenedFileswill be changed. To prevent this, use something that will be actually unique, such asname,tabId.Hope this helps :)