Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Run useStoreState selector even when the store is undefined #2760

Merged
merged 2 commits into from
Aug 24, 2023
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
5 changes: 5 additions & 0 deletions .changeset/2760-real-toes-prove.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ariakit/react-core": minor
---

[`#2760`](https://github.com/ariakit/ariakit/pull/2760) **BREAKING**: The `useStoreState` function exported by `@ariakit/react-core/utils/store` has been updated so it'll always run the selector callback even when the passed store is `null` or `undefined`.
9 changes: 4 additions & 5 deletions packages/ariakit-react-core/src/checkbox/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ export const useCheckbox = createHook<CheckboxOptions>(
const context = useCheckboxContext();
store = store || context;

const storeChecked = useStoreState(store, (state) => {
const [_checked, setChecked] = useState(defaultChecked ?? false);

const checked = useStoreState(store, (state) => {
if (checkedProp !== undefined) return checkedProp;
if (state.value === undefined) return;
if (state?.value === undefined) return _checked;
if (valueProp != null) {
if (Array.isArray(state.value)) {
const nonArrayValue = getNonArrayValue(valueProp);
Expand All @@ -71,9 +73,6 @@ export const useCheckbox = createHook<CheckboxOptions>(
return false;
});

const [_checked, setChecked] = useState(defaultChecked ?? false);
const checked = checkedProp ?? storeChecked ?? _checked;

const ref = useRef<HTMLInputElement>(null);
const tagName = useTagName(ref, props.as || "input");
const nativeCheckbox = isNativeCheckbox(tagName, props.type);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,10 @@ export function useCollectionRenderer<T extends Item = any>({
const context = useCollectionContext();
store = store || (context as typeof store);

const items =
useStoreState(store, (state) => itemsProp ?? (state.items as T[])) ||
itemsProp;
const items = useStoreState(
store,
(state) => itemsProp ?? (state?.items as T[]),
);

invariant(
items != null,
Expand Down
19 changes: 12 additions & 7 deletions packages/ariakit-react-core/src/composite/composite-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ export const useCompositeItem = createHook<CompositeItemOptions>(
const row = useContext(CompositeRowContext);
const rowId = useStoreState(store, (state) => {
if (rowIdProp) return rowIdProp;
if (!state) return;
if (!row?.baseElement) return;
if (row.baseElement !== state.baseElement) return;
return row.id;
Expand Down Expand Up @@ -314,7 +315,7 @@ export const useCompositeItem = createHook<CompositeItemOptions>(

const baseElement = useStoreState(
store,
(state) => state.baseElement || undefined,
(state) => state?.baseElement || undefined,
);

const providerValue = useMemo(
Expand All @@ -332,7 +333,10 @@ export const useCompositeItem = createHook<CompositeItemOptions>(
[providerValue],
);

const isActiveItem = useStoreState(store, (state) => state.activeId === id);
const isActiveItem = useStoreState(
store,
(state) => !!state && state.activeId === id,
);
const virtualFocus = useStoreState(store, "virtualFocus");
const role = useRole(ref, props);
let ariaSelected: boolean | undefined;
Expand All @@ -353,13 +357,15 @@ export const useCompositeItem = createHook<CompositeItemOptions>(

const ariaSetSize = useStoreState(store, (state) => {
if (ariaSetSizeProp != null) return ariaSetSizeProp;
if (!state) return;
if (!row?.ariaSetSize) return;
if (row.baseElement !== state.baseElement) return;
return row.ariaSetSize;
});

const ariaPosInSet = useStoreState(store, (state) => {
if (ariaPosInSetProp != null) return ariaPosInSetProp;
if (!state) return;
if (!row?.ariaPosInSet) return;
if (row.baseElement !== state.baseElement) return;
const itemsInRow = state.renderedItems.filter(
Expand All @@ -368,11 +374,10 @@ export const useCompositeItem = createHook<CompositeItemOptions>(
return row.ariaPosInSet + itemsInRow.findIndex((item) => item.id === id);
});

const isTabbable =
useStoreState(store, (state) => {
if (!state.renderedItems.length) return true;
return !state.virtualFocus && state.activeId === id;
}) ?? true;
const isTabbable = useStoreState(store, (state) => {
if (!state?.renderedItems.length) return true;
return !state.virtualFocus && state.activeId === id;
});

props = {
id,
Expand Down
22 changes: 10 additions & 12 deletions packages/ariakit-react-core/src/composite/composite-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,16 @@ export function useCompositeRenderer<T extends Item = any>({
const context = useContext(CompositeContext);
store = store || (context as typeof store);

const orientation =
useStoreState(store, (state) =>
orientationProp ?? state.orientation === "both"
? "vertical"
: state.orientation,
) ?? orientationProp;

const items =
useStoreState(store, (state) => {
if ("mounted" in state && state.mounted) return 0;
return props.items ?? (state.items as T[]);
}) || props.items;
const orientation = useStoreState(store, (state) =>
orientationProp ?? state?.orientation === "both"
? "vertical"
: state?.orientation,
);

const items = useStoreState(store, (state) => {
if (state && "mounted" in state && state.mounted) return 0;
return props.items ?? (state?.items as T[]);
});

const id = useId(props.id);

Expand Down
4 changes: 2 additions & 2 deletions packages/ariakit-react-core/src/menu/menu-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ export const useMenuList = createHook<MenuListOptions>(
state.orientation === "both" ? undefined : state.orientation,
);
const isHorizontal = orientation !== "vertical";
const isMenuBarHorizontal = !!useStoreState(
const isMenuBarHorizontal = useStoreState(
parentMenuBar,
(state) => state.orientation !== "vertical",
(state) => !!state && state.orientation !== "vertical",
);

const onKeyDown = useEvent((event: KeyboardEvent<HTMLDivElement>) => {
Expand Down
2 changes: 1 addition & 1 deletion packages/ariakit-react-core/src/menu/menu-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function useMenuStoreOptions<T extends Values = Values>(
parentMenu || parentMenuBar,
(state) =>
placementProp ||
(state.orientation === "vertical" ? "right-start" : "bottom-start"),
(state?.orientation === "vertical" ? "right-start" : "bottom-start"),
);

const parentIsMenuBar = !!parentMenuBar && !parentMenu;
Expand Down
9 changes: 4 additions & 5 deletions packages/ariakit-react-core/src/radio/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,10 @@ export const useRadio = createHook<RadioOptions>(
const id = useId(props.id);

const ref = useRef<HTMLInputElement>(null);
const isChecked =
useStoreState(
store,
(state) => checked ?? getIsChecked(value, state.value),
) ?? checked;
const isChecked = useStoreState(
store,
(state) => checked ?? getIsChecked(value, state?.value),
);

// When the radio store has a default value, we need to update the active id
// to point to the checked element, otherwise it'll be the first item in the
Expand Down
10 changes: 4 additions & 6 deletions packages/ariakit-react-core/src/select/select-renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,11 @@ function useSelectRenderer<T extends Item = any>({
const context = useContext(SelectContext);
store = store || context;

const items =
useStoreState(store, (state) =>
state.mounted ? itemsProp ?? (state.items as T[]) : 0,
) ?? itemsProp;
const items = useStoreState(store, (state) =>
state?.mounted ? itemsProp ?? (state.items as T[]) : 0,
);

const value =
useStoreState(store, (state) => valueProp ?? state.value) ?? valueProp;
const value = useStoreState(store, (state) => valueProp ?? state?.value);

const valueIndices = useMemo(() => {
if (!items) return [];
Expand Down
59 changes: 25 additions & 34 deletions packages/ariakit-react-core/src/utils/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
} from "@ariakit/core/utils/store";
import type {
AnyFunction,
AnyObject,
PickByValue,
SetState,
} from "@ariakit/core/utils/types";
Expand Down Expand Up @@ -49,7 +50,6 @@ type UseState<S> = {

type StateStore<T = CoreStore> = T | null | undefined;
type StateKey<T = CoreStore> = keyof StoreState<T>;
type StateSelector<T = CoreStore, V = any> = (state: StoreState<T>) => V;

const noopSubscribe = () => () => {};

Expand All @@ -76,44 +76,35 @@ function safeFlushSync(fn: AnyFunction, canFlushSync = true) {
}
}

/**
* Subscribes to and returns the entire state of a store.
* @param store The store to subscribe to.
* @example
* useStoreState(store)
*/
export function useStoreState<T extends StateStore>(
store: T,
): T extends CoreStore ? StoreState<T> : undefined;
export function useStoreState<T extends CoreStore>(store: T): StoreState<T>;

/**
* Subscribes to and returns a specific key of a store.
* @param store The store to subscribe to.
* @param key The key to subscribe to.
* @example
* useStoreState(store, "key")
*/
export function useStoreState<T extends StateStore, K extends StateKey<T>>(
export function useStoreState<T extends CoreStore>(
store: T | null | undefined,
): StoreState<T> | undefined;

export function useStoreState<T extends CoreStore, K extends StateKey<T>>(
store: T,
key: K,
): T extends CoreStore ? StoreState<T>[K] : undefined;
): StoreState<T>[K];

/**
* Subscribes to and returns a computed value of a store based on the selector
* function.
* @param store The store to subscribe to.
* @param selector The selector function that computes the observed value.
* @example
* useStoreState(store, (state) => state.key)
*/
export function useStoreState<T extends StateStore, V>(
export function useStoreState<T extends CoreStore, K extends StateKey<T>>(
store: T | null | undefined,
key: K,
): StoreState<T>[K] | undefined;

export function useStoreState<T extends CoreStore, V>(
store: T,
selector: StateSelector<T, V>,
): T extends CoreStore ? V : undefined;
selector: (state: StoreState<T>) => V,
): V;

export function useStoreState<T extends CoreStore, V>(
store: T | null | undefined,
selector: (state?: StoreState<T>) => V,
): V;

export function useStoreState(
store: StateStore,
keyOrSelector: StateKey | StateSelector = identity,
keyOrSelector: StateKey | ((state?: AnyObject) => any) = identity,
) {
const storeSubscribe = React.useCallback(
(callback: () => void) => {
Expand All @@ -124,11 +115,11 @@ export function useStoreState(
);

const getSnapshot = () => {
if (!store) return;
const state = store.getState();
const selector = typeof keyOrSelector === "function" ? keyOrSelector : null;
const key = typeof keyOrSelector === "string" ? keyOrSelector : null;
const selector = typeof keyOrSelector === "function" ? keyOrSelector : null;
const state = store?.getState();
if (selector) return selector(state);
if (!state) return;
if (!key) return;
if (!hasOwnProperty(state, key)) return;
return state[key];
Expand Down
Loading