Skip to content

Commit

Permalink
Merge branch 'compiler'
Browse files Browse the repository at this point in the history
  • Loading branch information
jmeistrich committed Nov 13, 2024
2 parents edecede + 3ee80aa commit 83d605a
Show file tree
Hide file tree
Showing 17 changed files with 233 additions and 152 deletions.
157 changes: 53 additions & 104 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"json": "^11.0.0",
"ksuid": "^3.0.0",
"next": "^14",
"prettier": "3.3.3",
"react": "18.3.1",
Expand Down Expand Up @@ -138,4 +137,4 @@
"@commitlint/config-conventional"
]
}
}
}
1 change: 1 addition & 0 deletions react.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './src/react/Computed';
export * from './src/react/$';
export * from './src/react/For';
export { usePauseProvider } from './src/react/usePauseProvider';
export * from './src/react/Memo';
Expand Down
45 changes: 29 additions & 16 deletions src/config/enableReactTracking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,42 +8,55 @@ import { __SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED as ReactInternals }
interface ReactTrackingOptions {
auto?: boolean; // Make all get() calls act as useSelector() hooks
warnUnobserved?: boolean; // Warn if get() is used outside of an observer
warnGet?: boolean; // Warn if get() is used in a component
}

export function enableReactTracking({ auto, warnUnobserved }: ReactTrackingOptions) {
export function enableReactTracking({ auto, warnUnobserved, warnGet }: ReactTrackingOptions) {
const { get } = internal;

if (auto || (process.env.NODE_ENV === 'development' && warnUnobserved)) {
if (auto || (process.env.NODE_ENV === 'development' && (warnUnobserved || warnGet))) {
const ReactRenderContext = createContext(0);

const isInRender = () => {
// If we're already tracking then we definitely don't need useSelector
try {
// If there's no dispatcher we're definitely not in React
// This is an optimization to not need to run useContext. If in a future React version
// this works differently we can change it or just remove it.
const dispatcher = ReactInternals.ReactCurrentDispatcher.current;
if (dispatcher) {
// If there's a dispatcher then we may be inside of a hook.
// Attempt a useContext hook, which will throw an error if outside of render.
useContext(ReactRenderContext);
return true;
}
} catch {} // eslint-disable-line no-empty
return false;
};

const needsSelector = () => {
// If we're already tracking then we definitely don't need useSelector
if (!tracking.current) {
try {
// If there's no dispatcher we're definitely not in React
// This is an optimization to not need to run useContext. If in a future React version
// this works differently we can change it or just remove it.
const dispatcher = ReactInternals.ReactCurrentDispatcher.current;
if (dispatcher) {
// If there's a dispatcher then we may be inside of a hook.
// Attempt a useContext hook, which will throw an error if outside of render.
useContext(ReactRenderContext);
return true;
}
} catch {} // eslint-disable-line no-empty
return isInRender();
}
return false;
};

configureLegendState({
observableFunctions: {
get: (node: NodeInfo, options?: TrackingType | (GetOptions & UseSelectorOptions)) => {
if (needsSelector()) {
if (process.env.NODE_ENV === 'development' && warnUnobserved) {
if (isInRender()) {
console.warn(
'[legend-state] Detected a `get()` call in a React component. It is recommended to use the `use$` hook instead to be compatible with React Compiler: https://legendapp.com/open-source/state/v3/react/react-api/#use$',
);
}
} else if (needsSelector()) {
if (auto) {
return useSelector(() => get(node, options), isObject(options) ? options : undefined);
} else if (process.env.NODE_ENV === 'development' && warnUnobserved) {
console.warn(
'[legend-state] Detected a `get()` call in an unobserved component. You may want to wrap it in observer: https://legendapp.com/open-source/state/react-api/#observer-hoc',
'[legend-state] Detected a `get()` call in an unobserved component. You may want to wrap it in observer: https://legendapp.com/open-source/state/v3/react/react-api/#observer',
);
}
}
Expand Down
11 changes: 8 additions & 3 deletions src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,21 @@ import {
isPrimitive,
isSet,
} from './is';
import type { Change, ObserveEvent, OpaqueObject, Selector, TypeAtPath } from './observableInterfaces';
import type { Change, GetOptions, ObserveEvent, OpaqueObject, Selector, TypeAtPath } from './observableInterfaces';
import type { ObservableParam } from './observableTypes';

export function computeSelector<T>(selector: Selector<T>, e?: ObserveEvent<T>, retainObservable?: boolean): T {
export function computeSelector<T>(
selector: Selector<T>,
getOptions?: GetOptions,
e?: ObserveEvent<T>,
retainObservable?: boolean,
): T {
let c = selector as any;
if (!isObservable(c) && isFunction(c)) {
c = e ? c(e) : c();
}

return isObservable(c) && !retainObservable ? c.get() : c;
return isObservable(c) && !retainObservable ? c.get(getOptions) : c;
}

export function getObservableIndex(value$: ObservableParam): number {
Expand Down
2 changes: 1 addition & 1 deletion src/observableInterfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Observable, ObservableParam } from './observableTypes';
export type TrackingType = undefined | true | symbol; // true === shallow

export interface GetOptions {
shallow: boolean;
shallow?: boolean;
}

export type OpaqueObject<T> = T & { [symbolOpaque]: true };
Expand Down
6 changes: 5 additions & 1 deletion src/observe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ export function observe<T>(
// Dispose listeners from previous run
dispose?.();

const { dispose: _dispose, value, nodes } = trackSelector(selectorOrRun as Selector<T>, update, e, options);
const {
dispose: _dispose,
value,
nodes,
} = trackSelector(selectorOrRun as Selector<T>, update, undefined, e, options);
dispose = _dispose;

e.value = value;
Expand Down
59 changes: 59 additions & 0 deletions src/react/$.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { ComponentProps, memo, NamedExoticComponent, ReactElement } from 'react';
import { Computed } from './Computed';
import { isEmpty, isFunction } from '@legendapp/state';
import { createElement, FC, forwardRef } from 'react';
import { ReactiveFnBinders, ReactiveFns } from './configureReactive';
import { reactive } from './reactive-observer';
import { IReactive } from '@legendapp/state/react';

type ComputedWithMemo = (params: {
children: ComponentProps<typeof Computed>['children'];
scoped?: boolean;
}) => ReactElement;

const Memo = memo(Computed as ComputedWithMemo, (prev, next) =>
next.scoped ? prev.children === next.children : true,
) as NamedExoticComponent<{
children: any;
scoped?: boolean;
}>;

type ReactiveProxy = typeof Memo & IReactive;

const setReactProps = new Set([
'$$typeof',
'defaultProps',
'propTypes',
'tag',
'PropTypes',
'displayName',
'getDefaultProps',
'type',
'compare',
]);

const reactives: Record<string, FC> = {};

export const $: ReactiveProxy = new Proxy(Memo as any, {
get(target: Record<string, FC>, p: string) {
if (Object.hasOwn(target, p) || setReactProps.has(p)) {
return target[p];
}
if (!reactives[p]) {
const Component = ReactiveFns.get(p) || p;

// Create a wrapper around createElement with the string so we can proxy it
// eslint-disable-next-line react/display-name
const render = forwardRef((props, ref) => {
const propsOut = { ...props } as any;
if (ref && (isFunction(ref) || !isEmpty(ref))) {
propsOut.ref = ref;
}
return createElement(Component, propsOut);
});

reactives[p] = reactive(render, [], ReactiveFnBinders.get(p));
}
return reactives[p];
},
}) as unknown as ReactiveProxy;
14 changes: 12 additions & 2 deletions src/react/Memo.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { memo } from 'react';
import { memo, ReactElement, NamedExoticComponent, ComponentProps } from 'react';
import { Computed } from './Computed';

export const Memo = memo(Computed, () => true);
type ComputedWithMemo = (params: {
children: ComponentProps<typeof Computed>['children'];
scoped?: boolean;
}) => ReactElement;

export const Memo = memo(Computed as ComputedWithMemo, (prev, next) =>
next.scoped ? prev.children === next.children : true,
) as NamedExoticComponent<{
children: any;
scoped?: boolean;
}>;
4 changes: 2 additions & 2 deletions src/react/reactInterfaces.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Observable, Selector } from '@legendapp/state';
import type { GetOptions, Observable, Selector } from '@legendapp/state';
import type { FC, LegacyRef, ReactNode } from 'react';

export type ShapeWithNew$<T> = Partial<Omit<T, 'children'>> & {
Expand Down Expand Up @@ -29,7 +29,7 @@ export type FCReactive<P, P2> = P &
}
>;

export interface UseSelectorOptions {
export interface UseSelectorOptions extends GetOptions {
suspense?: boolean;
skipCheck?: boolean;
}
2 changes: 1 addition & 1 deletion src/react/useObserve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function useObserve<T>(

if (!ref.current.dispose) {
ref.current.dispose = observe<T>(
((e: ObserveEventCallback<T>) => computeSelector(ref.current.selector, e)) as any,
((e: ObserveEventCallback<T>) => computeSelector(ref.current.selector, undefined, e)) as any,
(e) => ref.current.reaction?.(e),
options,
);
Expand Down
6 changes: 4 additions & 2 deletions src/react/useSelector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function createSelectorFunctions<T>(
value,
dispose: _dispose,
resubscribe: _resubscribe,
} = trackSelector(_selector, _update, undefined, undefined, /*createResubscribe*/ true);
} = trackSelector(_selector, _update, options, undefined, undefined, /*createResubscribe*/ true);

dispose = _dispose;
resubscribe = _resubscribe;
Expand Down Expand Up @@ -116,7 +116,7 @@ export function useSelector<T>(selector: Selector<T>, options?: UseSelectorOptio
// Short-circuit to skip creating the hook if selector is an observable
// and running in an observer. If selector is a function it needs to run in its own context.
if (reactGlobals.inObserver && isObservable(selector) && !options?.suspense) {
return computeSelector(selector);
return computeSelector(selector, options);
}

let value;
Expand Down Expand Up @@ -163,3 +163,5 @@ export function useSelector<T>(selector: Selector<T>, options?: UseSelectorOptio

return value;
}

export { useSelector as use$ };
14 changes: 4 additions & 10 deletions src/sync-plugins/keel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
WaitForSetCrudFnParams,
syncedCrud,
} from '@legendapp/state/sync-plugins/crud';
import ksuid from 'ksuid';

// Keel types
export interface KeelObjectBase {
Expand Down Expand Up @@ -49,10 +48,6 @@ type Result<T, U> = NonNullable<Data<T> | Err<U>>;

type SubscribeFn = (params: SyncedGetSetSubscribeBaseParams) => () => void;

export function generateKeelId() {
return ksuid.randomSync().string;
}

export interface KeelGetParams {}

export interface KeelListParams<Where = {}> {
Expand Down Expand Up @@ -99,7 +94,7 @@ interface SyncedKeelPropsManyWhere<
CrudResult<
APIResult<{
results: TRemote[];
pageInfo: any;
pageInfo?: any;
}>
>
>;
Expand All @@ -111,7 +106,7 @@ interface SyncedKeelPropsManyNoWhere<TRemote extends { id: string }, TLocal, AOp
CrudResult<
APIResult<{
results: TRemote[];
pageInfo: any;
pageInfo?: any;
}>
>
>;
Expand Down Expand Up @@ -150,7 +145,7 @@ export interface SyncedKeelPropsBase<TRemote extends { id: string }, TLocal = TR
> {
client?: KeelClient;
create?: (i: NoInfer<Partial<TRemote>>) => Promise<APIResult<NoInfer<TRemote>>>;
update?: (params: { where: any; values?: Partial<TRemote> }) => Promise<APIResult<TRemote>>;
update?: (params: { where: any; values?: NoInfer<Partial<TRemote>> }) => Promise<APIResult<NoInfer<TRemote>>>;
delete?: (params: { id: string }) => Promise<APIResult<string>>;
realtime?: {
path?: (action: string, inputs: any) => string | Promise<string>;
Expand Down Expand Up @@ -275,7 +270,7 @@ async function getAllPages<TRemote>(
listFn: (params: KeelListParams<any>) => Promise<
APIResult<{
results: TRemote[];
pageInfo: any;
pageInfo?: any;
}>
>,
params: KeelListParams,
Expand Down Expand Up @@ -584,7 +579,6 @@ export function syncedKeel<
changesSince,
updatePartial: true,
subscribe,
generateId: generateKeelId,
get,
}) as SyncedCrudReturnType<TLocal, TOption>;
}
7 changes: 5 additions & 2 deletions src/trackSelector.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { computeSelector } from './helpers';
import type { ObserveOptions, ListenerParams, ObserveEvent, Selector } from './observableInterfaces';
import type { ObserveOptions, ListenerParams, ObserveEvent, Selector, GetOptions } from './observableInterfaces';
import { setupTracking } from './setupTracking';
import { beginTracking, endTracking, tracking } from './tracking';

export function trackSelector<T>(
selector: Selector<T>,
update: (params: ListenerParams) => void,
getOptions?: GetOptions,
observeEvent?: ObserveEvent<T>,
observeOptions?: ObserveOptions,
createResubscribe?: boolean,
Expand All @@ -15,7 +16,9 @@ export function trackSelector<T>(
let updateFn = update;

beginTracking();
const value = selector ? computeSelector(selector, observeEvent, observeOptions?.fromComputed) : selector;
const value = selector
? computeSelector(selector, getOptions, observeEvent, observeOptions?.fromComputed)
: selector;
const tracker = tracking.current;
const nodes = tracker!.nodes;
endTracking();
Expand Down
11 changes: 7 additions & 4 deletions src/when.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isObservable } from './globals';
import { computeSelector, isObservableValueReady } from './helpers';
import { isArray, isPromise } from './is';
import { isArray, isFunction, isPromise } from './is';
import type { ObserveEvent, Selector } from './observableInterfaces';
import { observe } from './observe';

Expand Down Expand Up @@ -29,10 +29,13 @@ function _when<T, T2>(predicate: Selector<T> | Selector<T>[], effect?: (value: T
let isOk: any = true;
if (isArray(ret)) {
for (let i = 0; i < ret.length; i++) {
if (isObservable(ret[i])) {
ret[i] = computeSelector(ret[i]);
let item = ret[i];
if (isObservable(item)) {
item = computeSelector(item);
} else if (isFunction(item)) {
item = item();
}
isOk = isOk && !!(checkReady ? isObservableValueReady(ret[i]) : ret[i]);
isOk = isOk && !!(checkReady ? isObservableValueReady(item) : item);
}
} else {
isOk = checkReady ? isObservableValueReady(ret) : ret;
Expand Down
Loading

0 comments on commit 83d605a

Please sign in to comment.