Skip to content

Commit 4b7ba0b

Browse files
committed
use 'derive state from props' pattern to sync Tabs internal state with props
1 parent c7583a2 commit 4b7ba0b

File tree

1 file changed

+102
-60
lines changed

1 file changed

+102
-60
lines changed

src/lib/tabs-ui/useTabs.tsx

Lines changed: 102 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,29 @@ import React, {
88
import { TabModel } from "src/lib/tabs-ui/tabs-ui.types.ts";
99
import { removeItem } from "src/utils/array-utils.ts";
1010

11+
type StateListener = (state: State, partialState: Partial<State>) => void;
12+
13+
type Disposer = () => void;
14+
1115
export type TabsApi = {
12-
setTabs: (tabs: TabModel[], runHandlers?: boolean) => void;
13-
setActiveTabId: (id: string | undefined, runHandlers?: boolean) => void;
16+
listeners: Set<StateListener>;
17+
subscribe: (cb: StateListener) => Disposer;
18+
setTabs: (tabs: TabModel[], emitEvents?: boolean) => void;
19+
setActiveTabId: (id: string | undefined, emitEvents?: boolean) => void;
1420
getState: () => State;
1521
getActiveTab: () => TabModel | undefined;
1622
setState: (
1723
state: Partial<State> | { (prevState: State): Partial<State> },
18-
runHandlers?: boolean,
24+
emitEvents?: boolean,
1925
) => void;
2026
forceUpdate: () => void;
2127
closeTab: (tab: TabModel) => void;
2228
registerChildTabsApi: (childTabsApi: TabsApi) => void;
2329
unregisterChildTabsApi: () => void;
2430
registerParentTabsApi: (parentTabsApi: TabsApi) => void;
2531
unregisterParentTabsApi: () => void;
32+
setStartPinnedTabs: (ids: string[], emitEvents: boolean) => void;
33+
setEndPinnedTabs: (ids: string[], emitEvents: boolean) => void;
2634
};
2735

2836
export type TabsProps = {
@@ -95,33 +103,28 @@ const useActive = (apiRef: MutableRefObject<TabsApi>, props: TabsProps) => {
95103
const { hasControlledActiveTabId, activeTabId } = props;
96104
apiRef.current["setActiveTabId"] = (
97105
id: string | undefined,
98-
runHandlers = true,
106+
emitEvents = true,
99107
) => {
100108
apiRef.current.setState(
101109
{
102110
activeTabId: id,
103111
},
104-
runHandlers,
112+
emitEvents,
105113
);
106114
apiRef.current.forceUpdate();
107115
};
108116
apiRef.current["getActiveTab"] = () => {
109117
const { tabs, activeTabId } = apiRef.current.getState();
110118
return tabs.find((tab) => tab.id === activeTabId);
111119
};
112-
useEffect(() => {
113-
if (hasControlledActiveTabId) {
114-
apiRef.current.setActiveTabId(activeTabId, false);
115-
}
116-
}, [hasControlledActiveTabId, activeTabId, apiRef]);
120+
const state = apiRef.current.getState();
121+
if (hasControlledActiveTabId && state.activeTabId !== activeTabId) {
122+
apiRef.current.setActiveTabId(activeTabId, false);
123+
}
117124
};
118125

119126
const useTabsState = (apiRef: MutableRefObject<TabsApi>, props: TabsProps) => {
120127
const {
121-
onTabsChange,
122-
onActiveTabIdChange,
123-
onStartPinnedTabsChange,
124-
onEndPinnedTabsChange,
125128
initialTabs = [],
126129
initialActiveTabId,
127130
initialStartPinnedTabs = [],
@@ -143,35 +146,23 @@ const useTabsState = (apiRef: MutableRefObject<TabsApi>, props: TabsProps) => {
143146

144147
apiRef.current["setState"] = (
145148
stateOrFn: Partial<State> | { (state: State): Partial<State> },
146-
runHandlers = true,
149+
emitEvents = true,
147150
) => {
148151
const newState =
149152
typeof stateOrFn === "function" ? stateOrFn(stateRef.current) : stateOrFn;
150153

151-
const subStateKeys: (keyof State)[] = Object.keys(
152-
newState,
153-
) as unknown as (keyof State)[];
154-
155-
if (runHandlers) {
156-
subStateKeys.forEach((subStateKey) => {
157-
const subState = newState[subStateKey];
158-
const subStateChangeHandler = {
159-
tabs: onTabsChange,
160-
activeTabId: onActiveTabIdChange,
161-
startPinnedTabs: onStartPinnedTabsChange,
162-
endPinnedTabs: onEndPinnedTabsChange,
163-
childTabsApi: () => {},
164-
parentTabsApi: () => {},
165-
}[subStateKey];
166-
// @ts-ignore
167-
subStateChangeHandler?.(subState);
168-
});
169-
}
170-
171-
stateRef.current = {
154+
const mergedNewState = {
172155
...stateRef.current,
173156
...newState,
174157
};
158+
159+
if (emitEvents) {
160+
[...apiRef.current.listeners].forEach((listener) => {
161+
listener(mergedNewState, newState);
162+
});
163+
}
164+
165+
stateRef.current = mergedNewState;
175166
};
176167
};
177168

@@ -181,42 +172,46 @@ const usePinning = (apiRef: MutableRefObject<TabsApi>, props: TabsProps) => {
181172
endPinnedTabs: endPinnedTabsProp,
182173
} = props;
183174
const setStartPinnedTabs = useCallback(
184-
(ids: string[], runHandlers = true) => {
175+
(ids: string[], emitEvents = true) => {
185176
apiRef.current.setState(
186177
{
187178
startPinnedTabs: ids,
188179
},
189-
runHandlers,
180+
emitEvents,
190181
);
191182
apiRef.current.forceUpdate();
192183
},
193184
[apiRef],
194185
);
195186

196187
const setEndPinnedTabs = useCallback(
197-
(ids: string[], runHandlers = true) => {
188+
(ids: string[], emitEvents = true) => {
198189
apiRef.current.setState(
199190
{
200191
endPinnedTabs: ids,
201192
},
202-
runHandlers,
193+
emitEvents,
203194
);
204195
apiRef.current.forceUpdate();
205196
},
206197
[apiRef],
207198
);
199+
apiRef.current["setStartPinnedTabs"] = setStartPinnedTabs;
200+
apiRef.current["setEndPinnedTabs"] = setEndPinnedTabs;
201+
const state = apiRef.current.getState();
208202

209-
useEffect(() => {
210-
if (startPinnedTabsProp) {
211-
setStartPinnedTabs(startPinnedTabsProp, false);
212-
}
213-
}, [startPinnedTabsProp, setStartPinnedTabs]);
214-
215-
useEffect(() => {
216-
if (endPinnedTabsProp) {
217-
setEndPinnedTabs(endPinnedTabsProp, false);
218-
}
219-
}, [endPinnedTabsProp, setEndPinnedTabs]);
203+
if (
204+
startPinnedTabsProp &&
205+
state.startPinnedTabs.join("") != startPinnedTabsProp.join("")
206+
) {
207+
apiRef.current.setStartPinnedTabs(startPinnedTabsProp, false);
208+
}
209+
if (
210+
endPinnedTabsProp &&
211+
state.endPinnedTabs.join("") != endPinnedTabsProp.join("")
212+
) {
213+
apiRef.current.setEndPinnedTabs(endPinnedTabsProp, false);
214+
}
220215
};
221216

222217
const useChildTabsApi = (apiRef: MutableRefObject<TabsApi>) => {
@@ -258,32 +253,79 @@ const useChildTabsApi = (apiRef: MutableRefObject<TabsApi>) => {
258253
};
259254

260255
const useTabModels = (apiRef: MutableRefObject<TabsApi>, props: TabsProps) => {
261-
const { tabs: tabsProp } = props;
262-
apiRef.current["setTabs"] = (tabs: TabModel[], runHandlers = true) => {
256+
const { tabs: tabsFromProps } = props;
257+
258+
apiRef.current["setTabs"] = (tabs: TabModel[], emitEvents = true) => {
263259
apiRef.current.setState(
264260
{
265261
tabs,
266262
},
267-
runHandlers,
263+
emitEvents,
268264
);
269265
apiRef.current.forceUpdate();
270266
};
271267

272-
useEffect(() => {
273-
if (tabsProp) {
274-
apiRef.current.setTabs(tabsProp, false);
275-
}
276-
}, [tabsProp, apiRef]);
268+
const { tabs: prevTabs } = apiRef.current.getState();
269+
270+
const hashTabs = (tabs: TabModel[]) => {
271+
return tabs.map((tab) => tab.id).join("");
272+
};
273+
274+
if (tabsFromProps && hashTabs(prevTabs) != hashTabs(tabsFromProps)) {
275+
apiRef.current.setTabs(tabsFromProps, false);
276+
}
277277
};
278278

279279
export const useTabs = (
280280
apiRef: MutableRefObject<TabsApi>,
281281
props: TabsProps,
282282
) => {
283+
const {
284+
onTabsChange,
285+
onActiveTabIdChange,
286+
onStartPinnedTabsChange,
287+
onEndPinnedTabsChange,
288+
} = props;
283289
apiRef.current["forceUpdate"] = useForceRerender();
284290

285-
useTabModels(apiRef, props);
291+
if (!apiRef.current.listeners) {
292+
apiRef.current.listeners = new Set<StateListener>();
293+
}
294+
295+
apiRef.current["subscribe"] = (cb: StateListener) => {
296+
apiRef.current.listeners.add(cb);
297+
return () => {
298+
apiRef.current.listeners.delete(cb);
299+
};
300+
};
301+
302+
useEffect(() => {
303+
const handlersMap = {
304+
tabs: onTabsChange,
305+
activeTabId: onActiveTabIdChange,
306+
startPinnedTabs: onStartPinnedTabsChange,
307+
endPinnedTabs: onEndPinnedTabsChange,
308+
};
309+
return apiRef.current.subscribe((_, partialState) => {
310+
Object.keys(partialState).forEach((subStateKey) => {
311+
const subState = partialState[subStateKey as keyof State];
312+
// @ts-ignore
313+
const subStateChangeHandler = handlersMap[subStateKey];
314+
// @ts-ignore
315+
subStateChangeHandler?.(subState);
316+
});
317+
});
318+
}, [
319+
apiRef,
320+
onTabsChange,
321+
onActiveTabIdChange,
322+
onStartPinnedTabsChange,
323+
onEndPinnedTabsChange,
324+
]);
325+
286326
useTabsState(apiRef, props);
327+
useTabModels(apiRef, props);
328+
287329
useClosing(apiRef);
288330
usePinning(apiRef, props);
289331
useActive(apiRef, props);

0 commit comments

Comments
 (0)