Skip to content

Commit

Permalink
test: add tests and additional docs
Browse files Browse the repository at this point in the history
  • Loading branch information
hlysine committed Jun 10, 2023
1 parent dae214a commit d9286ab
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 13 deletions.
90 changes: 80 additions & 10 deletions src/__tests__/component.test.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import React, { useState } from 'react';
import { act, render } from '@testing-library/react';
import { act, render, renderHook } from '@testing-library/react';
import '@testing-library/jest-dom';
import {
isReactive,
isReadonly,
isShallow,
makeReactive,
reactive,
ref,
useComputed,
useReactive,
useReactiveRerender,
useReference,
useWatch,
useWatchEffect,
Expand All @@ -27,6 +31,53 @@ afterEach(() => {
jest.restoreAllMocks();
});

describe('useReactiveRerender', () => {
it('returns a valid object', () => {
const { result } = renderHook(() =>
useReactiveRerender({ a: 1, b: 'abc' })
);
expect(result.current.a).toBe(1);
expect(isReactive(result.current)).toBe(true);
expect(isReadonly(result.current)).toBe(true);
expect(isShallow(result.current)).toBe(true);
});
it('keeps the same instance across re-render', () => {
const { result, rerender } = renderHook((obj: any = { a: 1, b: 'abc' }) =>
useReactiveRerender(obj)
);
const ref = result.current;

rerender();

expect(result.current).toBe(ref);
expect(result.current.a).toBe(1);

rerender({ a: 2, b: 'def' });

expect(result.current).toBe(ref);
expect(result.current.a).toBe(2);
});
it('triggers reactive effects', async () => {
const mockEffect = jest.fn();
const Tester = function Tester(props: { a: number }) {
props = useReactiveRerender(props);
useWatchEffect(() => {
mockEffect(props.a);
});
return <p>{props.a}</p>;
};
const { findByText, rerender } = render(<Tester a={1} />);
const content = await findByText('1');
expect(content).toBeTruthy();
expect(mockEffect).toBeCalledTimes(1);

rerender(<Tester a={2} />);

expect(mockEffect).toBeCalledTimes(2);
expect(mockEffect).toBeCalledWith(2);
});
});

describe('makeReactive', () => {
it('renders without crashing', async () => {
const Tester = makeReactive(function Tester() {
Expand Down Expand Up @@ -80,39 +131,58 @@ describe('makeReactive', () => {
expect(content2).toBeTruthy();
});
it('accepts props', async () => {
const Tester = makeReactive(function Tester({
value,
onChange,
}: {
const Tester = makeReactive(function Tester(props: {
value: string;
onChange: () => void;
obj: { a: number; b: number };
}) {
expect(isReactive(props)).toBe(true);
expect(isReadonly(props)).toBe(true);
expect(isReactive(props.obj)).toBe(false);
expect(isReadonly(props.obj)).toBe(false);
return (
<>
<p>{value}</p>
<input onChange={onChange} value={value} />
<p>{props.value}</p>
<input onChange={props.onChange} value={props.value} />
</>
);
});

const { findByText } = render(
<Tester value="Test component" onChange={() => {}} />
<Tester value="Test component" onChange={() => {}} obj={{ a: 1, b: 2 }} />
);
const content = await findByText('Test component');
expect(content).toBeTruthy();
});
it('updates props', async () => {
const Tester = makeReactive(function Tester({ value }: { value: string }) {
return <p>{value}</p>;
const mockEffect = jest.fn();
const Tester = makeReactive(function Tester(props: { value?: string }) {
useWatchEffect(() => {
mockEffect(props.value);
});
return <p>{props.value}</p>;
});

const { findByText, rerender } = render(<Tester value="Test component" />);
const content = await findByText('Test component');
expect(content).toBeTruthy();
expect(mockEffect).toHaveBeenCalledTimes(1);

rerender(<Tester value="Test component 2" />);
const content2 = await findByText('Test component 2');
expect(content2).toBeTruthy();
expect(mockEffect).toHaveBeenCalledTimes(2);

rerender(<Tester value="Test component 2" />);
const content3 = await findByText('Test component 2');
expect(content3).toBeTruthy();
expect(mockEffect).toHaveBeenCalledTimes(2);

rerender(<Tester />);
await expect(
async () => await findByText('Test component 2')
).rejects.toThrow();
expect(mockEffect).toHaveBeenCalledTimes(3);
});
it('re-renders when ref changes', async () => {
const count = ref(0);
Expand Down
19 changes: 16 additions & 3 deletions src/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ interface ReactiveRerenderRef<T> {
* This hook converts data in React's reactivity system to reactive data that is compatible with the rest of this
* library. You should use other hooks to create reactive data from source if possible, but if not, you can use this
* hook to convert the data. A typical use case is to pass values from `useState` hooks or React contexts into this
* function so that reactive effects, such as `useWatchEffect`, can react to data changes from those hooks.
* function so that reactive effects, such as `useWatchEffect`, can react to data changes from those hooks. You can also
* use this hook to create reactive props if the component is not already wrapped with `makeReactive`.
*
* @example
* Converting a value from `useState` to a reactive object. Note that a better solution is to replace `useState` with
Expand All @@ -67,6 +68,16 @@ interface ReactiveRerenderRef<T> {
* });
* ```
*
* @example
* Creating reactive props. Components wrapped with `makeReactive` already have reactive props, so you don't need this
* hook in those components.
* ```jsx
* function App(props) {
* props = useReactiveRerender(props);
* return <div>{props.count}</div>;
* }
* ```
*
* @param target The data to be made reactive.
* @returns A reactive object that maintains the same instance across re-renders.
*/
Expand Down Expand Up @@ -161,7 +172,8 @@ function useReactivityInternals<P extends {}>(

interface MakeReactive {
/**
* Converts a function component into a reactive component.
* Converts a function component into a reactive component. A reactive component receives reactive props and
* re-renders automatically when its data dependencies are modified.
*
* If your function component makes use of a reactive value, the component has to be wrapped by `makeReactive` so
* that it can re-render when the reactive value changes.
Expand Down Expand Up @@ -191,7 +203,8 @@ interface MakeReactive {
*/
<P extends {}>(component: React.FC<P>): React.FC<P>;
/**
* Converts a custom hook to be reactive.
* Converts a custom hook to be reactive. A reactive component receives reactive props and
* re-renders automatically when its data dependencies are modified.
*
* If your custom hook makes use of a reactive value, the function has to be wrapped by `makeReactive` so
* that it can trigger a re-render on the component when the reactive value changes.
Expand Down

0 comments on commit d9286ab

Please sign in to comment.