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 element
which updates the state and set the active
flag to trueclose
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 :)