Coder Social home page Coder Social logo

react-facade's Introduction

React Facade

Write components with placeholders for hooks.

An experimental 2kb library that uses Proxy and TypeScript to create a strongly typed facade for your React hooks.

  • Dependency inversion between components and hooks: Build components that rely on hooks that do not have a particular implementation.
  • Works with all of your existing hooks: Extract hooks to the top level of your program and replace them with a facade.
  • Simplified component testing: You were already testing your hooks anyway (right?), so why test them again? Focus on rendering outcomes rather than bungling around with a cumbersome setup for test cases.

The hooks implementation is injected via React context so that they can be changed between views or testing. It is dependency injection for hooks that does not require higher-order functions/Components.

Example

Consider an application which describes its data-fetching layer in the UI with the following list of hooks,

function useCurrentUser(): User;
function usePostById(id: string): { loading: boolean; error?: Error; data?: Post };
function useCreateNewPost(): (postData: PostData) => Promise<Post>;
// etc...

Given this interface, a developer can reliably use these hooks without knowing anything about their underlying implementation (leaving aside questions of fidelity). That is to say: the developer only cares about the interface. The problem, however, is that by inlining a hook as part of the React component, the implementation cannot be ignored. For example, a component UserProfile may have the following definition,

// user-profile.tsx

import React from "react";
import { userCurrentUser } from "./hooks";

export function UserProfile() {
  const user = useCurrentUser();

  // ... render user profile
}

The developer of this component may not care about the implementation of useCurrentUser, but the tests sure do! If under the hood useCurrentUser calls the react-redux useSelector, then UserProfile depends directly on a global Redux store. Moreover, any component using UserProfile also has this dependency. The coupling between the store and component tree is hard-coded by this hook. Yikes!

Consider the same problem where the implementation can be completely ignored and replaced by dependency injection. We define the same interface using createFacade,

// facade.ts

import { createFacade } from "react-facade";

type Hooks = {
  useCurrentUser(): User;
  usePostById(id: string): { loading?: boolean; error?: Error; data?: Post };
  useCreateNewPost(): (postData: PostData) => Promise<Post>;
  // ...
};

// no implementation!
export const [hooks, ImplementationProvider] = createFacade<Hooks>();

And then the UserProfile becomes,

// user-profile.tsx

import React from "react";
import { hooks } from "./facade";

export function UserProfile() {
  const user = hooks.useCurrentUser();

  // ... render user profile
}

This time, we don't care about the implementation because there literally isn't one. Depending on the environment, it can be injected by passing a different implementation to ImplementationProvider.

At the application level, we might use useSelector to fetch the current user from our store,

// app.tsx

import React from "react";
import { useSelector } from "react-redux";
import { ImplementationProvider } from "./facade";
// ...

const implementation = {
  useCurrentUser(): User {
    return useSelector(getCurrentUser);
  },

  // ...
};

return (
  <ImplementationProvider implementation={implementation}>
    <UserProfile />
  </ImplementationProvider>
);

While in a test environment, we can return a stub user so long as it matches our interface,

// user-profile.test.tsx

import React from "react";
import { render } from "@testing-library/react";
import { ImplementationProvider } from "./facade";
// ...

test("some thing", () => {
  const implementation = {
    useCurrentUser(): User {
      return {
        id: "stub",
        name: "Gabe",
        // ...
      };
    },

    // ...
  };

  const result = render(
    // What is `__UNSAFE_Test_Partial`? See API section
    <ImplementationProvider.__UNSAFE_Test_Partial implementation={implementation}>
      <UserProfile />
    </ImplementationProvider.__UNSAFE_Test_Partial>
  );

  // ...
});

We are programming purely toward the interface and NOT the implementation!

Again, consider how this might simplify testing a component that relied on this hook,

function usePostById(id: string): { loading: boolean; error?: Error; data?: Post };

Testing different states is simply a matter of declaratively passing in the right one,

// post.test.tsx

const loadingImplementation = {
  usePostById(id: string) {
    return {
      loading: true,
    };
  },
};

const errorImplementation = {
  usePostById(id: string) {
    return {
      loading: false,
      error: new Error("uh oh!"),
    };
  },
};

// ...

test("shows the loading spinner", () => {
  const result = render(
    <ImplementationProvider.__UNSAFE_Test_Partial implementation={loadingImplementation}>
      <Post id={id} />
    </ImplementationProvider.__UNSAFE_Test_Partial>
  );

  // ...
});

test("displays an error", () => {
  const result = render(
    <ImplementationProvider.__UNSAFE_Test_Partial implementation={errorImplementation}>
      <Post id={id} />
    </ImplementationProvider.__UNSAFE_Test_Partial>
  );

  // ...
});

API

createFacade

type Wrapper = React.JSXElementConstructor<{ children: React.ReactElement }>;

function createFacade<T>(
  options?: Partial<{ displayName: string; strict: boolean; wrapper: Wrapper }>
): [Proxy<T>, ImplementationProvider<T>];

Takes a type definition T - which must be an object where each member is a function - and returns the tuple of the interface T (via a Proxy) and an ImplementationProvider. The developer provides the real implementation of the interface through the Provider.

The ImplementationProvider does not collide with other ImplementationProviders created by other createFacade calls, so you can make as many of these as you need.

Options

option type default details
displayName string "Facade" The displayName for debugging with React Devtools .
strict boolean true When true does not allow the implementation to change between renders.
wrapper React.FC ({ children }) => children A wrapper component that can be used to wrap the ImplementationProvider.

ImplementationProvider<T>

Accepts a prop implementation: T that implements the interface defined in createFacade<T>().

const implementation = {
  useCurrentUser(): User {
    return useSelector(getCurrentUser);
  },

  // ...
};

return (
  <ImplementationProvider implementation={implementation}>
    <UserProfile />
  </ImplementationProvider>
);

ImplementationProvider<T>.__UNSAFE_Test_Partial

Used for partially implementing the interface when you don't need to implement the whole thing but still want it to type-check (tests?). For the love of God, please do not use this outside of tests...

<ImplementationProvider.__UNSAFE_Test_Partial implementation={partialImplementation}>
  <UserProfile />
</ImplementationProvider.__UNSAFE_Test_Partial>

Installing

npm install react-facade

Asked Questions

Why not just use jest.mock?

Mocking at the module level has the notable downside that type safety is optional. The onus is on the developer to ensure that the mock matches the actual interface. While stubbing with a static language is dangerous enough because it removes critical interactions between units of code, a dynamic language is even worse because changes to the real implementation interface (without modifications to the stub) can result in runtime type errors in production. Choosing to forgo the type check means that you might as well be writing JavaScript.

Can I use this with plain JavaScript?

It is really important that this library is used with TypeScript. It's a trick to use a Proxy object in place of the real implementation when calling createFacade, so nothing stops you from calling a function that does not exist. Especially bad would be destructuring so your fake hook could be used elsewhere in the program.

// hooks.js

export const { useSomethingThatDoesNotExist } = hooks;
// my-component.jsx

import { useSomethingThatDoesNotExist } from "./hooks";

const MyComponent = () => {
  const value = useSomethingThatDoesNotExist(); // throw new Error('oopsie-doodle!')
};

The only thing preventing you from cheating like this is good ol' TypeScript.

Is this safe to use?

Popular libraries like immer use the same trick of wrapping data T in a Proxy and present it as T, so I don't think you should be concerned. Proxy has ~95% browser support.

react-facade's People

Contributors

garbles avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

react-facade's Issues

Allow objects in the facade, not just functions

I would like to separate hooks inside different categories, and I think it would be nice if we had the option to be able to define a facade like:

export type Hooks = {
  useHook1: typeof useHook1
  category: {
    useHook2: typeof useHook2
  }
}

Right now, the createFacade function leaves out every non function property.

I suggest that we should allow mocking everything, or at least objects. I don't see any issue allowing devs to mock non-functions as well. If someone for some reason wants to mock a string, I think we should not care.

I tried to edit the code myself but I don't really understand the comment that explains why there is a fallback function for this. Is this challenging to implement?

createFacade type error with interface

createFacade errors when being used with the interface type. Typescript recommends interfaces over intersections.

type Type = { useX: () => void };
interface Interface { useX: () => void };
createFacade<Type>;
createFacade<Interface>; // Type 'Interface' does not satisfy the constraint 'FacadeInterface'. Index signature for type 'string' is missing in type 'Interface'.

Codesandbox

Maybe the type could be improved, or is the only way around using type instead of interface? I am using this rule to enforce the use of interfaces over types so it will require an exception to the rule :)

Great library btw! Looking forward to start using it more

Where is this going?

At a first glance this idea looks pretty neat. But it looks like this never got any traction. Any particular reason why? Is there a simpler pattern? I would think about using this for stories in a storybook, but the implementation of the hooks might get quite far from the story component (somewhere in a decorator) and the whole thing might become a little difficult to handle. Any opinions?

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.