diff --git a/src/react-atom-internal.ts b/src/react-atom-internal.tsx similarity index 92% rename from src/react-atom-internal.ts rename to src/react-atom-internal.tsx index 26aa822..88ea7d9 100644 --- a/src/react-atom-internal.ts +++ b/src/react-atom-internal.tsx @@ -15,7 +15,7 @@ import { setValidator, swap } from "@libre/atom"; -import { SetStateAction, useLayoutEffect, useMemo, useState } from "react"; +import React, { SetStateAction, useLayoutEffect, useMemo, useState } from "react"; import * as ErrorMsgs from "./error-messages"; import { HookDependencies, PublicExports, ReactUseStateHook, UseAtomOptions } from "./internal-types"; @@ -77,7 +77,7 @@ export function initialize(hooks: HookDependencies): PublicExports { let hook: ReactUseStateHook; try { selector = useMemo(() => memoLast(selector), [select]); - [, hook] = useState({}) as [{}, ReactUseStateHook]; + [, hook] = (useState({}) as unknown) as [{}, ReactUseStateHook]; } catch (err) { throw new TypeError(ErrorMsgs.calledUseAtomOutsideFunctionComponent); } @@ -186,6 +186,19 @@ export function useAtom(atom: Atom, options?: UseAtomOptions) { return select ? internalUseAtom(atom, { select }) : internalUseAtom(atom); } +// =========================== CONNECTATOM ==================================== +export function connectAtom(atom: Atom, mapStateToProps: (state: A) => S) { + return function

(Component: React.ComponentType) { + const wrapper: React.FC

= (props: P) => { + const state = useAtom(atom); + const stateProps = mapStateToProps(state); + return ; + }; + + return wrapper; + }; +} + /** * default instance of useAtom */ diff --git a/test/connectAtom.spec.tsx b/test/connectAtom.spec.tsx new file mode 100644 index 0000000..c5ae2be --- /dev/null +++ b/test/connectAtom.spec.tsx @@ -0,0 +1,98 @@ +import { connectAtom } from "../src/react-atom-internal"; +import * as React from "react"; +import { Atom, deref, swap } from "@libre/atom"; +import { cleanup, render, getByTestId } from "react-testing-library"; +import { act } from "react-dom/test-utils"; + +interface ITestAtom { + count: number; + notACount: string; +} + +interface ITestProps { + testProp: string; +} + +interface ITestStateProps { + countFromState: number; +} + +const TEST_ATOM = Atom.of({ count: 3, notACount: "kek" }); +let timesRendered = 0; + +class TestComponent extends React.Component { + render() { + timesRendered += 1; + const props = this.props; + return ( +

+

{props.testProp}

+

{props.countFromState}

+
+ ); + } +} + +describe("connectAtom function", () => { + afterEach(() => { + // WARNING! DON'T CHANGE THE ORDER OF THESE: + timesRendered = 0; + cleanup(); + // END WARNING + }); + + it("connectAtom is a function", () => { + expect(connectAtom).toBeInstanceOf(Function); + }); + + it("should connect atom to class component without errors", () => { + connectAtom(TEST_ATOM, state => ({ countFromState: state.count }))(TestComponent); + }); + + it("returns the state of the connected atom", () => { + const ConnectedComponent = connectAtom(TEST_ATOM, state => ({ countFromState: state.count }))(TestComponent); + const { container } = render(); + const actualCountFromState = getByTestId(container, "countFromState").textContent; + const expectedCountFromState = deref(TEST_ATOM).count.toString(); + + expect(actualCountFromState).toBe(expectedCountFromState); + }); + + it("returns component props", () => { + const ConnectedComponent = connectAtom(TEST_ATOM, state => ({ countFromState: state.count }))(TestComponent); + const expectedTestProp = "test prop"; + const { container } = render(); + const actualTestProp = getByTestId(container, "testProp").textContent; + + expect(actualTestProp).toBe(expectedTestProp); + }); + + it("returns the same values across multiple renders", () => { + const ConnectedComponent = connectAtom(TEST_ATOM, state => ({ countFromState: state.count }))(TestComponent); + const { container, rerender } = render(); + const statePropValue = getByTestId(container, "countFromState").textContent; + const propValue = getByTestId(container, "testProp").textContent; + + for (let i = 0; i < 10; i++) { + rerender(); + let statePropValueAfterRerender = getByTestId(container, "countFromState").textContent; + let propValueAfterRerender = getByTestId(container, "testProp").textContent; + expect(statePropValueAfterRerender).toBe(statePropValue); + expect(propValueAfterRerender).toBe(propValue); + } + }); + + it("returns changed state of the atom after swap", () => { + const ConnectedComponent = connectAtom(TEST_ATOM, state => ({ countFromState: state.count }))(TestComponent); + const { container } = render(); + let actualCountFromState = getByTestId(container, "countFromState").textContent; + let expectedCountFromState = deref(TEST_ATOM).count.toString(); + expect(actualCountFromState).toBe(expectedCountFromState); + act(() => { + swap(TEST_ATOM, state => ({ ...state, count: 8 })); + }); + actualCountFromState = getByTestId(container, "countFromState").textContent; + expectedCountFromState = deref(TEST_ATOM).count.toString(); + expect(actualCountFromState).toBe(expectedCountFromState); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 06c12b0..dd5c3bd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,5 @@ "target": "es5", "typeRoots": ["node_modules/@types"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "src/react-atom-internal.tsx"] }