Skip to content

Commit

Permalink
feat: Make $ double as Reactive
Browse files Browse the repository at this point in the history
  • Loading branch information
jmeistrich committed Nov 9, 2024
1 parent 2e1f471 commit bad339a
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 2 deletions.
49 changes: 47 additions & 2 deletions src/react/$.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,59 @@
import { memo, ReactElement, NamedExoticComponent, ComponentProps } from 'react';
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;

export const $ = memo(Computed as ComputedWithMemo, (prev, next) =>
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;
39 changes: 39 additions & 0 deletions tests/react.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1756,6 +1756,45 @@ describe('Reactive', () => {
// expect(items[0].id).toEqual('1');
});
});
describe('$', () => {
test('$.div $className', () => {
const obs$ = observable('hi');
let num = 0;
const Test = function Test() {
return (
<$.div
$className={() => {
num++;
return obs$.get();
}}
/>
);
};
function App() {
return createElement(Test);
}
render(createElement(App));

expect(num).toEqual(1);
const { container } = render(createElement(Test));

let items = container.querySelectorAll('div');
expect(items.length).toEqual(1);
expect(items[0].className).toEqual('hi');

act(() => {
obs$.set('hello');
});

items = container.querySelectorAll('div');

expect(items[0].className).toEqual('hello');

// items = container.querySelectorAll('li');
// expect(items.length).toEqual(2);
// expect(items[0].id).toEqual('1');
});
});
describe('Memo', () => {
test('Memo works with function returning function', () => {
let num = 0;
Expand Down

0 comments on commit bad339a

Please sign in to comment.