Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/tame-swans-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphiql/plugin-history': patch
'@graphiql/react': minor
'graphiql': patch
---

feat(@graphiql/react): migrate React context to zustand, replace `useStorageContext` with `useStorage` hook

6 changes: 3 additions & 3 deletions examples/graphiql-webpack/src/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import 'graphiql/style.css';
import '@graphiql/plugin-explorer/style.css';
import '@graphiql/plugin-code-exporter/style.css';
import { createGraphiQLFetcher } from '@graphiql/toolkit';
import { useStorageContext } from '@graphiql/react';
import { useStorage } from '@graphiql/react';

export const STARTING_URL =
'https://swapi-graphql.netlify.app/.netlify/functions/index';
Expand Down Expand Up @@ -60,9 +60,9 @@ const style = { height: '100vh' };
const explorer = explorerPlugin();

const App = () => {
const storage = useStorageContext();
const storage = useStorage({ nonNull: true });

const lastUrl = storage?.get(LAST_URL_KEY);
const lastUrl = storage.get(LAST_URL_KEY);
const [currentUrl, setUrl] = React.useState(lastUrl ?? STARTING_URL);
// TODO: a breaking change where we make URL an internal state concern, and then expose hooks
// so that you can handle/set URL state internally from a plugin
Expand Down
8 changes: 4 additions & 4 deletions examples/graphiql-webpack/src/select-server-plugin.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import * as React from 'react';

import './select-server-plugin.css';
import { useStorageContext, useSchemaContext } from '@graphiql/react';
import { useStorage, useSchemaContext } from '@graphiql/react';

export const LAST_URL_KEY = 'lastURL';

export const PREV_URLS_KEY = 'previousURLs';

const SelectServer = ({ url, setUrl }) => {
const inputRef = React.useRef(null);
const storage = useStorageContext();
const lastUrl = storage?.get(LAST_URL_KEY);
const storage = useStorage({ nonNull: true });
const lastUrl = storage.get(LAST_URL_KEY);
const currentUrl = lastUrl ?? url;
const [inputValue, setInputValue] = React.useState(currentUrl);
const [previousUrls, setPreviousUrls] = React.useState(
JSON.parse(storage?.get(PREV_URLS_KEY)) ?? [currentUrl],
JSON.parse(storage.get(PREV_URLS_KEY)) ?? [currentUrl],
);
const [error, setError] = React.useState(null);

Expand Down
4 changes: 2 additions & 2 deletions packages/graphiql-plugin-history/src/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { createStore, StoreApi, useStore } from 'zustand';
import { HistoryStore, QueryStoreItem, StorageAPI } from '@graphiql/toolkit';
import {
useStorageContext,
useStorage,
useExecutionContext,
useEditorContext,
} from '@graphiql/react';
Expand Down Expand Up @@ -141,7 +141,7 @@ export const HistoryContextProvider: FC<HistoryContextProviderProps> = ({
maxHistoryLength = 20,
children,
}) => {
const storage = useStorageContext();
const storage = useStorage();
const { isFetching } = useExecutionContext({ nonNull: true });
const { tabs, activeTabIndex } = useEditorContext({ nonNull: true });
const activeTab = tabs[activeTabIndex];
Expand Down
3 changes: 2 additions & 1 deletion packages/graphiql-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"get-value": "^3.0.1",
"graphql-language-service": "^5.3.1",
"markdown-it": "^14.1.0",
"set-value": "^4.1.0"
"set-value": "^4.1.0",
"zustand": "^5"
},
"devDependencies": {
"babel-plugin-react-compiler": "19.1.0-rc.1",
Expand Down
4 changes: 2 additions & 2 deletions packages/graphiql-react/src/editor/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
import { VariableToType } from 'graphql-language-service';
import { FC, ReactNode, useEffect, useRef, useState } from 'react';

import { useStorageContext } from '../storage';
import { useStorage } from '../storage';
import { createContextHook, createNullableContext } from '../utility/context';
import { STORAGE_KEY as STORAGE_KEY_HEADERS } from './header-editor';
import { useSynchronizeValue } from './hooks';
Expand Down Expand Up @@ -255,7 +255,7 @@ type EditorContextProviderProps = {
};

export const EditorContextProvider: FC<EditorContextProviderProps> = props => {
const storage = useStorageContext();
const storage = useStorage();
const [headerEditor, setHeaderEditor] = useState<CodeMirrorEditor | null>(
null,
);
Expand Down
4 changes: 2 additions & 2 deletions packages/graphiql-react/src/editor/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { parse, print } from 'graphql';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { usePluginContext } from '../plugin';
import { useSchemaContext } from '../schema';
import { useStorageContext } from '../storage';
import { useStorage } from '../storage';
import { debounce } from '../utility';
import { onHasCompletion } from './completion';
import { useEditorContext } from './context';
Expand Down Expand Up @@ -47,7 +47,7 @@ export function useChangeHandler(
caller: Function,
) {
const { updateActiveTabValues } = useEditorContext({ nonNull: true, caller });
const storage = useStorageContext();
const storage = useStorage();

useEffect(() => {
if (!editor) {
Expand Down
4 changes: 2 additions & 2 deletions packages/graphiql-react/src/editor/query-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { useExecutionContext } from '../execution';
import { markdown } from '../markdown';
import { usePluginContext } from '../plugin';
import { useSchemaContext } from '../schema';
import { useStorageContext } from '../storage';
import { useStorage } from '../storage';
import { debounce } from '../utility/debounce';
import {
commonKeys,
Expand Down Expand Up @@ -147,7 +147,7 @@ export function useQueryEditor(
caller: caller || _useQueryEditor,
});
const executionContext = useExecutionContext();
const storage = useStorageContext();
const storage = useStorage();
const plugin = usePluginContext();
const copy = useCopyQuery({ caller: caller || _useQueryEditor, onCopyQuery });
const merge = useMergeQuery({ caller: caller || _useQueryEditor });
Expand Down
6 changes: 1 addition & 5 deletions packages/graphiql-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,7 @@ export {
SchemaContextProvider,
useSchemaContext,
} from './schema';
export {
StorageContext,
StorageContextProvider,
useStorageContext,
} from './storage';
export { StorageContextProvider, useStorage } from './storage';
export { useTheme } from './theme';

export * from './utility';
Expand Down
4 changes: 2 additions & 2 deletions packages/graphiql-react/src/plugin.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ComponentType, FC, ReactNode, useEffect, useState } from 'react';
import { useStorageContext } from './storage';
import { useStorage } from './storage';
import { createContextHook, createNullableContext } from './utility';

export type GraphiQLPlugin = {
Expand Down Expand Up @@ -75,7 +75,7 @@ export const PluginContextProvider: FC<PluginContextProviderProps> = ({
plugins: $plugins,
referencePlugin,
}) => {
const storage = useStorageContext();
const storage = useStorage();
const plugins = (() => {
const pluginList: GraphiQLPlugin[] = [];
const pluginTitles: Record<string, true> = {};
Expand Down
54 changes: 44 additions & 10 deletions packages/graphiql-react/src/storage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
import { Storage, StorageAPI } from '@graphiql/toolkit';
import { FC, ReactNode, useEffect, useRef, useState } from 'react';
import { createContextHook, createNullableContext } from './utility/context';
import {
createContext,
FC,
ReactNode,
RefObject,
useContext,
useEffect,
useRef,
} from 'react';
import { create, StoreApi, useStore } from 'zustand';

export type StorageContextType = StorageAPI;
export type StorageContextType = {
storage: StorageAPI | null;
};

export const StorageContext =
createNullableContext<StorageContextType>('StorageContext');
const StorageContext = createContext<RefObject<
StoreApi<StorageContextType>
> | null>(null);

type StorageContextProviderProps = {
children: ReactNode;
Expand All @@ -23,21 +34,44 @@ export const StorageContextProvider: FC<StorageContextProviderProps> = ({
children,
}) => {
const isInitialRender = useRef(true);
const [$storage, setStorage] = useState(() => new StorageAPI(storage));
const storeRef = useRef<StoreApi<StorageContextType>>(null!);

if (storeRef.current === null) {
storeRef.current = create<StorageContextType>()(() => ({
storage: new StorageAPI(storage),
}));
}

useEffect(() => {
if (isInitialRender.current) {
isInitialRender.current = false;
} else {
setStorage(new StorageAPI(storage));
return;
}
storeRef.current.setState({ storage: new StorageAPI(storage) });
}, [storage]);

return (
<StorageContext.Provider value={$storage}>
<StorageContext.Provider value={storeRef}>
{children}
</StorageContext.Provider>
);
};

export const useStorageContext = createContextHook(StorageContext);
const defaultStore = create<StorageContextType>()(() => ({
storage: null,
}));

function useStorage(): StorageAPI | null;
function useStorage(options: { nonNull: true }): StorageAPI;
function useStorage(options: { nonNull: boolean }): StorageAPI | null;
function useStorage(options?: { nonNull?: boolean }): StorageAPI | null {
const store = useContext(StorageContext);
if (options?.nonNull && !store) {
throw new Error(
'Tried to use `useStorage` without the necessary context. Make sure to render the `StorageContextProvider` component higher up the tree.',
);
}
return useStore(store ? store.current : defaultStore, state => state.storage);
}

export { useStorage };
12 changes: 6 additions & 6 deletions packages/graphiql-react/src/theme.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { useStorageContext } from './storage';
import { useStorage } from './storage';

/**
* The value `null` semantically means that the user does not explicitly choose
Expand All @@ -8,14 +8,14 @@ import { useStorageContext } from './storage';
export type Theme = 'light' | 'dark' | null;

export function useTheme(defaultTheme: Theme = null) {
const storageContext = useStorageContext();
const storage = useStorage();

const [theme, setThemeInternal] = useState<Theme>(() => {
if (!storageContext) {
if (!storage) {
return null;
}

const stored = storageContext.get(STORAGE_KEY);
const stored = storage.get(STORAGE_KEY);
switch (stored) {
case 'light':
return 'light';
Expand All @@ -24,7 +24,7 @@ export function useTheme(defaultTheme: Theme = null) {
default:
if (typeof stored === 'string') {
// Remove the invalid stored value
storageContext.set(STORAGE_KEY, '');
storage.set(STORAGE_KEY, '');
}
return defaultTheme;
}
Expand All @@ -38,7 +38,7 @@ export function useTheme(defaultTheme: Theme = null) {
}, [theme]);

const setTheme = (newTheme: Theme) => {
storageContext?.set(STORAGE_KEY, newTheme || '');
storage?.set(STORAGE_KEY, newTheme || '');
setThemeInternal(newTheme);
};

Expand Down
2 changes: 1 addition & 1 deletion packages/graphiql-react/src/ui/tabs.css
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
-ms-overflow-style: none; /* IE and Edge */

&::-webkit-scrollbar {
@apply x:hidden; /* Chrome, Safari and Opera */
display: none; /* Chrome, Safari and Opera */
}
}

Expand Down
4 changes: 2 additions & 2 deletions packages/graphiql-react/src/utility/resize.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useEffect, useRef, useState } from 'react';
import { useStorageContext } from '../storage';
import { useStorage } from '../storage';
import { debounce } from './debounce';

type ResizableElement = 'first' | 'second';
Expand Down Expand Up @@ -53,7 +53,7 @@ export function useDragResize({
sizeThresholdSecond = 100,
storageKey,
}: UseDragResizeArgs) {
const storage = useStorageContext();
const storage = useStorage();

const store = debounce(500, (value: string) => {
if (storageKey) {
Expand Down
Loading
Loading