Skip to content

Commit 5985e13

Browse files
authored
feat(@graphiql/plugin-history): migrate React context to zustand, replace useHistoryContext with useHistory, useHistoryActions hooks (#3935)
* upd * upd * upd * upd * upd * prettier * Update .changeset/fuzzy-wolves-compete.md
1 parent 38fdcdb commit 5985e13

File tree

9 files changed

+154
-112
lines changed

9 files changed

+154
-112
lines changed

.changeset/fuzzy-wolves-compete.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@graphiql/plugin-history': minor
3+
'graphiql': patch
4+
---
5+
6+
feat(@graphiql/plugin-history): migrate React context to zustand, replace `useHistoryContext` with `useHistory`, `useHistoryActions` hooks

packages/graphiql-plugin-doc-explorer/vite.config.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import react from '@vitejs/plugin-react';
33
import type { PluginOptions as ReactCompilerConfig } from 'babel-plugin-react-compiler';
44
import packageJSON from './package.json' assert { type: 'json' };
55
import dts from 'vite-plugin-dts';
6+
import { reactCompilerConfig as $reactCompilerConfig } from '../graphiql-react/vite.config.mjs';
67

78
export const reactCompilerConfig: Partial<ReactCompilerConfig> = {
8-
target: '18',
9+
...$reactCompilerConfig,
910
sources(filename) {
1011
if (filename.includes('__tests__')) {
1112
return false;

packages/graphiql-plugin-history/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@
4141
"dependencies": {
4242
"react-compiler-runtime": "19.1.0-rc.1",
4343
"@graphiql/toolkit": "^0.11.2",
44-
"@graphiql/react": "^0.32.0"
44+
"@graphiql/react": "^0.32.0",
45+
"zustand": "^5"
4546
},
4647
"devDependencies": {
4748
"@testing-library/react": "^16.1.0",

packages/graphiql-plugin-history/src/components.tsx

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,26 +12,25 @@ import {
1212
Tooltip,
1313
UnStyledButton,
1414
} from '@graphiql/react';
15-
import { HistoryContextType, useHistoryContext } from './context';
15+
import { useHistory, useHistoryActions } from './context';
1616

1717
// Fix error from react-compiler
18-
// Support value blocks (conditional, logical, optional chaining, etc) within a try/catch statement
18+
// Support value blocks (conditional, logical, optional chaining, etc.) within a try/catch statement
1919
function handleDelete(
2020
items: QueryStoreItem[],
21-
deleteFromHistory: HistoryContextType['deleteFromHistory'],
21+
deleteFromHistory: ReturnType<typeof useHistoryActions>['deleteFromHistory'],
2222
) {
2323
for (const item of items) {
2424
deleteFromHistory(item, true);
2525
}
2626
}
2727

2828
export function History() {
29-
const { items: all, deleteFromHistory } = useHistoryContext({
30-
nonNull: true,
31-
});
29+
const all = useHistory();
30+
const { deleteFromHistory } = useHistoryActions();
3231

3332
// Reverse items since we push them in so want the latest one at the top, and pass the
34-
// original index in case multiple items share the same label so we can edit correct item
33+
// original index in case multiple items share the same label so we can edit the correct item
3534
let items = all
3635
.slice()
3736
.map((item, i) => ({ ...item, index: i }))
@@ -112,10 +111,7 @@ type QueryHistoryItemProps = {
112111

113112
export function HistoryItem(props: QueryHistoryItemProps) {
114113
const { editLabel, toggleFavorite, deleteFromHistory, setActive } =
115-
useHistoryContext({
116-
nonNull: true,
117-
caller: HistoryItem,
118-
});
114+
useHistoryActions();
119115
const { headerEditor, queryEditor, variableEditor } = useEditorContext({
120116
nonNull: true,
121117
caller: HistoryItem,
Lines changed: 125 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,80 +1,125 @@
1+
import {
2+
createContext,
3+
ReactNode,
4+
RefObject,
5+
useContext,
6+
useEffect,
7+
useRef,
8+
} from 'react';
9+
import { createStore, StoreApi, useStore } from 'zustand';
110
import { HistoryStore, QueryStoreItem, StorageAPI } from '@graphiql/toolkit';
2-
import { ReactNode, useEffect, useState } from 'react';
311
import {
412
useStorageContext,
5-
createNullableContext,
6-
createContextHook,
713
useExecutionContext,
814
useEditorContext,
915
} from '@graphiql/react';
1016

11-
export type HistoryContextType = {
12-
/**
13-
* Add an operation to the history.
14-
* @param operation The operation that was executed, consisting of the query,
15-
* variables, headers, and operation name.
16-
*/
17-
addToHistory(operation: {
18-
query?: string;
19-
variables?: string;
20-
headers?: string;
21-
operationName?: string;
22-
}): void;
17+
function createHistoryStore(
18+
storage: StorageAPI | null,
19+
maxHistoryLength: number,
20+
) {
21+
const historyStore =
22+
// Fall back to a noop storage when the StorageContext is empty
23+
new HistoryStore(storage || new StorageAPI(null), maxHistoryLength);
24+
25+
return createStore<HistoryContextType>(set => ({
26+
items: historyStore.queries,
27+
actions: {
28+
addToHistory(operation) {
29+
historyStore.updateHistory(operation);
30+
const items = historyStore.queries;
31+
set({ items });
32+
},
33+
editLabel(operation, index) {
34+
historyStore.editLabel(operation, index);
35+
const items = historyStore.queries;
36+
set({ items });
37+
},
38+
toggleFavorite(operation) {
39+
historyStore.toggleFavorite(operation);
40+
const items = historyStore.queries;
41+
set({ items });
42+
},
43+
setActive: item => item,
44+
deleteFromHistory(item, clearFavorites) {
45+
historyStore.deleteHistory(item, clearFavorites);
46+
const items = historyStore.queries;
47+
set({ items });
48+
},
49+
},
50+
}));
51+
}
52+
53+
type HistoryContextType = {
2354
/**
24-
* Change the custom label of an item from the history.
25-
* @param args An object containing the label (`undefined` if it should be
26-
* unset) and properties that identify the history item that the label should
27-
* be applied to. (This can result in the label being applied to multiple
28-
* history items.)
29-
* @param index Index to edit. Without it, will look for the first index matching the
30-
* operation, which may lead to misleading results if multiple items have the same label
55+
* The list of history items.
3156
*/
32-
editLabel(
33-
args: {
57+
items: readonly QueryStoreItem[];
58+
actions: {
59+
/**
60+
* Add an operation to the history.
61+
* @param operation The operation that was executed, consisting of the query,
62+
* variables, headers, and operation name.
63+
*/
64+
addToHistory(operation: {
65+
query?: string;
66+
variables?: string;
67+
headers?: string;
68+
operationName?: string;
69+
}): void;
70+
/**
71+
* Change the custom label of an item from the history.
72+
* @param args An object containing the label (`undefined` if it should be
73+
* unset) and properties that identify the history item that the label should
74+
* be applied to. (This can result in the label being applied to multiple
75+
* history items.)
76+
* @param index Index to edit. Without it, will look for the first index matching the
77+
* operation, which may lead to misleading results if multiple items have the same label
78+
*/
79+
editLabel(
80+
args: {
81+
query?: string;
82+
variables?: string;
83+
headers?: string;
84+
operationName?: string;
85+
label?: string;
86+
favorite?: boolean;
87+
},
88+
index?: number,
89+
): void;
90+
/**
91+
* Toggle the favorite state of an item from the history.
92+
* @param args An object containing the favorite state (`undefined` if it
93+
* should be unset) and properties that identify the history item that the
94+
* label should be applied to. (This can result in the label being applied
95+
* to multiple history items.)
96+
*/
97+
toggleFavorite(args: {
3498
query?: string;
3599
variables?: string;
36100
headers?: string;
37101
operationName?: string;
38102
label?: string;
39103
favorite?: boolean;
40-
},
41-
index?: number,
42-
): void;
43-
/**
44-
* The list of history items.
45-
*/
46-
items: readonly QueryStoreItem[];
47-
/**
48-
* Toggle the favorite state of an item from the history.
49-
* @param args An object containing the favorite state (`undefined` if it
50-
* should be unset) and properties that identify the history item that the
51-
* label should be applied to. (This can result in the label being applied
52-
* to multiple history items.)
53-
*/
54-
toggleFavorite(args: {
55-
query?: string;
56-
variables?: string;
57-
headers?: string;
58-
operationName?: string;
59-
label?: string;
60-
favorite?: boolean;
61-
}): void;
62-
/**
63-
* Delete an operation from the history.
64-
* @param args The operation that was executed, consisting of the query,
65-
* variables, headers, and operation name.
66-
* @param clearFavorites This is only if you press the 'clear' button
67-
*/
68-
deleteFromHistory(args: QueryStoreItem, clearFavorites?: boolean): void;
69-
/**
70-
* If you need to know when an item in history is set as active to customize
71-
* your application.
72-
*/
73-
setActive(args: QueryStoreItem): void;
104+
}): void;
105+
/**
106+
* Delete an operation from the history.
107+
* @param args The operation that was executed, consisting of the query,
108+
* variables, headers, and operation name.
109+
* @param clearFavorites This is only if you press the 'clear' button
110+
*/
111+
deleteFromHistory(args: QueryStoreItem, clearFavorites?: boolean): void;
112+
/**
113+
* If you need to know when an item in history is set as active to customize
114+
* your application.
115+
*/
116+
setActive(args: QueryStoreItem): void;
117+
};
74118
};
75119

76-
export const HistoryContext =
77-
createNullableContext<HistoryContextType>('HistoryContext');
120+
const HistoryContext = createContext<RefObject<
121+
StoreApi<HistoryContextType>
122+
> | null>(null);
78123

79124
type HistoryContextProviderProps = {
80125
children: ReactNode;
@@ -92,60 +137,46 @@ type HistoryContextProviderProps = {
92137
* to a backend instead of localStorage and might need an id property added to the QueryStoreItem)
93138
*/
94139
export function HistoryContextProvider({
95-
maxHistoryLength = DEFAULT_HISTORY_LENGTH,
140+
maxHistoryLength = 20,
96141
children,
97142
}: HistoryContextProviderProps) {
98143
const storage = useStorageContext();
99144
const { isFetching } = useExecutionContext({ nonNull: true });
100-
const [historyStore] = useState(
101-
() =>
102-
// Fall back to a noop storage when the StorageContext is empty
103-
new HistoryStore(storage || new StorageAPI(null), maxHistoryLength),
104-
);
105-
const [items, setItems] = useState(() => historyStore.queries || []);
106-
107-
const value: HistoryContextType = {
108-
addToHistory(operation) {
109-
historyStore.updateHistory(operation);
110-
setItems(historyStore.queries);
111-
},
112-
editLabel(operation, index) {
113-
historyStore.editLabel(operation, index);
114-
setItems(historyStore.queries);
115-
},
116-
items,
117-
toggleFavorite(operation) {
118-
historyStore.toggleFavorite(operation);
119-
setItems(historyStore.queries);
120-
},
121-
setActive: item => item,
122-
deleteFromHistory(item, clearFavorites) {
123-
historyStore.deleteHistory(item, clearFavorites);
124-
setItems(historyStore.queries);
125-
},
126-
};
127145
const { tabs, activeTabIndex } = useEditorContext({ nonNull: true });
128146
const activeTab = tabs[activeTabIndex];
129-
const { addToHistory } = value;
147+
const storeRef = useRef<StoreApi<HistoryContextType>>(null!);
148+
149+
if (storeRef.current === null) {
150+
storeRef.current = createHistoryStore(storage, maxHistoryLength);
151+
}
130152

131153
useEffect(() => {
132154
if (!isFetching) {
133155
return;
134156
}
157+
const { addToHistory } = storeRef.current.getState().actions;
135158
addToHistory({
136159
query: activeTab.query ?? undefined,
137160
variables: activeTab.variables ?? undefined,
138161
headers: activeTab.headers ?? undefined,
139162
operationName: activeTab.operationName ?? undefined,
140163
});
141-
}, [isFetching, activeTab, addToHistory]);
164+
}, [isFetching, activeTab]);
142165

143166
return (
144-
<HistoryContext.Provider value={value}>{children}</HistoryContext.Provider>
167+
<HistoryContext.Provider value={storeRef}>
168+
{children}
169+
</HistoryContext.Provider>
145170
);
146171
}
147172

148-
export const useHistoryContext =
149-
createContextHook<HistoryContextType>(HistoryContext);
173+
function useHistoryStore<T>(selector: (state: HistoryContextType) => T): T {
174+
const store = useContext(HistoryContext);
175+
if (!store) {
176+
throw new Error('Missing `HistoryContextProvider` in the tree');
177+
}
178+
return useStore(store.current, selector);
179+
}
150180

151-
const DEFAULT_HISTORY_LENGTH = 20;
181+
export const useHistory = () => useHistoryStore(state => state.items);
182+
export const useHistoryActions = () => useHistoryStore(state => state.actions);

packages/graphiql-plugin-history/src/index.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,7 @@ export const HISTORY_PLUGIN: GraphiQLPlugin = {
1212
export { History };
1313

1414
export {
15-
HistoryContext,
1615
HistoryContextProvider,
17-
useHistoryContext,
18-
type HistoryContextType,
16+
useHistory,
17+
useHistoryActions,
1918
} from './context';

packages/graphiql-plugin-history/vite.config.mts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import react from '@vitejs/plugin-react';
33
import type { PluginOptions as ReactCompilerConfig } from 'babel-plugin-react-compiler';
44
import packageJSON from './package.json' assert { type: 'json' };
55
import dts from 'vite-plugin-dts';
6+
import { reactCompilerConfig as $reactCompilerConfig } from '../graphiql-react/vite.config.mjs';
67

78
export const reactCompilerConfig: Partial<ReactCompilerConfig> = {
8-
target: '18',
9+
...$reactCompilerConfig,
910
sources(filename) {
1011
if (filename.includes('__tests__')) {
1112
return false;

packages/graphiql-react/src/utility/resize.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
2-
32
import { useStorageContext } from '../storage';
43
import { debounce } from './debounce';
54

@@ -9,6 +8,7 @@ type UseDragResizeArgs = {
98
/**
109
* Set the default sizes for the two resizable halves by passing their ratio
1110
* (first divided by second).
11+
* @default 1
1212
*/
1313
defaultSizeRelation?: number;
1414
/**
@@ -28,11 +28,13 @@ type UseDragResizeArgs = {
2828
/**
2929
* The minimum width in pixels for the first half. If it is resized to a
3030
* width smaller than this threshold, the half will be hidden.
31+
* @default 100
3132
*/
3233
sizeThresholdFirst?: number;
3334
/**
3435
* The minimum width in pixels for the second half. If it is resized to a
3536
* width smaller than this threshold, the half will be hidden.
37+
* @default 100
3638
*/
3739
sizeThresholdSecond?: number;
3840
/**

0 commit comments

Comments
 (0)