A reactive library for building Substrate front-end

7 Comments

NOTE: Polkassembly doesn't do code highlighting, so a much easier to read version can be found here instead.

Proposal for reactive-dot: A reactive library for building Substrate front-end

Overview

I'm Tien and was a lead front-end developer working in the Polkadot space for the past 1.5 years.

Now I'm going independent and I'd like to solve some developer experience issues I encountered along the way.

Prior to my involvement with Polkadot, I've worked on various projects within other ecosystems, namely Ethereum & Cosmos.

This library came about based on my experience building out front-ends with Polkadot.js, and comparing it with the developer experience available in Ethereum, realizing the same wasn't available for Polkadot.

This proposal outlines the creation of reactive-dot, a comprehensive React library designed to simplify and streamline the integration of Polkadot network functionalities into React applications. Inspired by the popular Wagmi library for Ethereum, reactive-dot aims to provide developers with an easy-to-use, modular, and flexible toolkit for interacting with the Polkadot ecosystem.

Objectives

  1. Simplify Development: Provide a set of intuitive React hooks to facilitate Polkadot network interactions, making it accessible for developers of all skill levels.
  2. Enhance Developer Experience: Reduce the boilerplate code and complexity involved in integrating Polkadot, allowing developers to focus on building robust applications.
  3. Promote Adoption: Encourage the adoption of Polkadot by lowering the entry barrier for developers through improved tooling.

Key features for the first version

Multichain support

Easy chain selection via React context.

const Root = () => (
  <ReDotProvider
    config={{
      providers: {
        "0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3":
          new WsProvider("wss://apps-rpc.polkadot.io"),
        "0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe":
          new WsProvider("wss://kusama-rpc.polkadot.io"),
      },
    }}
  >
    <ReDotChainProvider genesisHash="0x91b171bb158e2d3848fa23a9f1c25182fb8e20313b2c1eb49219da7a70ce90c3">
      <App />
    </ReDotChainProvider>
    <ReDotChainProvider genesisHash="0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe">
      <App />
    </ReDotChainProvider>
  </ReDotProvider>
);

Or via options override

const account = useQueryStorage("system", "account", [accountAddress], {
  genesisHash:
    "0xb0a8d493285c2df73290dfb7e61f870f17b41801197a149ca93654499ea3dafe",
});

Reading of storage

Access and read data stored in the Substrate-based storage directly from your React components.

// Reading single value
// this value is live from chain and will be updated automatically
const totalIssuance = useQueryStorage("balances", "totalIssuance", []);

console.log("Total issuance:", totalIssuance.toHuman());

// Reading multiple values
const poolMetadatum = useQueryStorage(
  "nominationPools",
  "metadata",
  [0, 1, 2, 3],
  {
    multi: true,
  },
);

for (const poolMetadata of poolMetadatum) {
  console.log("Pool name:", poolMetadata.toUtf8());
}

React suspense compatibility

React suspense are first class citizen for async & error handling.

const CurrentBlock = () => {
  const currentBlock = useQueryStorage("system", "number", []);

  return <p>Current block: {currentBlock}</p>;
};

const App = () => (
  <ErrorBoundary fallback="Error fetching block">
    <Suspense fallback="Loading block...">
      <CurrentBlock />
    </Suspense>
  </ErrorBoundary>
);

Caching, deduplication & persistent

Multiple reads of the same value throughout the application will only be fetched once, cached, and is kept up to date everywhere.

const myAccount = "SOME_ACCOUNT_ADDRESS";

const FreeBalance = () => {
  // First invocation will initiate subscription via web socket
  const account = useQueryStorage("system", "account", [myAccount]);

  return <p>Free balance: {account.data.free.toHuman()}</p>;
};

const FrozenBalance = () => {
  // Second invocation will only wait for and reuse value coming from the first invocation
  const account = useQueryStorage("system", "account", [myAccount]);

  return <p>Frozen balance: {account.data.frozen.toHuman()}</p>;
};

const ReservedBalance = () => {
  // Third invocation will also only wait for and reuse value coming from the first invocation
  const account = useQueryStorage("system", "account", [myAccount]);

  return <p>Reserved balance: {account.data.reserved.toHuman()}</p>;
};

const App = () => (
  <div>
    {/* `useQueryStorage("system", "account", [myAccount])` will only be executed once & is kept up to date for all 3 components */}
    <FreeBalance />
    <FrozenBalance />
    <ReservedBalance />
  </div>
);

Full TypeScript support with autocompletion

The library aim to provides strong TypeScript definition with 1-1 mapping to Substrate pallets definition.

Autocompletion

image

image

image

Strong return type definition

image

image

And more

The scope of this library can expand significantly based on community interest. Potential future features include:

  1. Wallet/account connections management
  2. Submitting transactions
  3. Utility hooks outside of reading storage
  4. Auto conversion of SCALE type encoding to native JS types (i.e. U32 -> Number, U256 -> BigInt, etc)
  5. Multi-adapter support: Polkadot.js, Polkadot-API, DeDot, etc
  6. Multi-framework support: React, Vue, Angular, etc
  7. Etc

Demo

A working proof of concept showcasing the library can be found here.

Code comparison

The below code snippets perform the following tasks:

  • Initiate connection to the chain
  • Reading the chain current block and the account balance
  • Display loading and error (if there's any) state
  • Display the final result after finished loading all values with no error

With Polkadot.js

import { ApiPromise, WsProvider } from "@polkadot/api";
import type { u32 } from "@polkadot/types-codec";
import type { FrameSystemAccountInfo } from "@polkadot/types/lookup";

const MY_ACCOUNT = "SOME_ADDRESS";
const LOADING = new Symbol();

const App = () => {
  const [api, setApi] = useState<ApiPromise | LOADING | Error>();
  const [currentBlock, setCurrentBlock] = useState<u32 | LOADING | Error>();
  const [account, setAccount] = useState<
    FrameSystemAccountInfo | LOADING | Error
  >();

  useEffect(() => {
    (async () => {
      setApi(LOADING);
      try {
        const api = await ApiPromise.create({
          provider: new WsProvider("wss://my.chain"),
        });
        setApi(api);
      } catch (error) {
        setApi(new Error("Unable to initialize ApiPromise", { cause: error }));
      }
    })();
  }, []);

  useEffect(() => {
    if (api === LOADING || api instanceof Error) {
      return;
    }

    const unsubscribePromise = (async () => {
      setCurrentBlock(LOADING);
      try {
        return api.query.system.number((currentBlock) =>
          setCurrentBlock(currentBlock),
        );
      } catch (error) {
        setCurrentBlock(
          new Error("Unable to get current block", { cause: error }),
        );
      }
    })();

    return () => {
      unsubscribePromise.then((unsubscribe) => {
        if (unsubscribe === undefined) {
          return;
        }

        unsubscribe();
      });
    };
  }, [api]);

  useEffect(() => {
    if (api === LOADING || api instanceof Error) {
      return;
    }

    const unsubscribePromise = (async () => {
      setAccount(LOADING);
      try {
        return api.query.system.account(MY_ACCOUNT, (account) =>
          setAccount(account),
        );
      } catch (error) {
        setAccount(new Error("Unable to get account", { cause: error }));
      }
    })();

    return () => {
      unsubscribePromise.then((unsubscribe) => {
        if (unsubscribe === undefined) {
          return;
        }

        unsubscribe();
      });
    };
  }, [api]);

  if (api === LOADING || currentBlock === LOADING || account === LOADING) {
    return <p>Loading...</p>;
  }

  if (
    api instanceof Error ||
    currentBlock instanceof Error ||
    account instanceof Error
  ) {
    return <p>Sorry, something went wrong.</p>;
  }

  return (
    <p>
      Your account free balance is: {account.data.free.toHuman()} at block{" "}
      {currentBlock.toNumber()}
    </p>
  );
};

With reactive-dot

const MY_ACCOUNT = "SOME_ADDRESS";

const _Balance = () => {
  const currentBlock = useQueryStorage("system", "number", []);
  const account = useQueryStorage("system", "account", [MY_ACCOUNT]);

  return (
    <p>
      Your account free balance is: {account.data.free.toHuman()} at block{" "}
      {currentBlock.toNumber()}
    </p>
  );
};

const Balance = () => (
  <ErrorBoundary fallback={<p>Sorry, something went wrong.</p>}>
    <Suspense fallback={<p>Loading...</p>}>
      <_Balance />
    </Suspense>
  </ErrorBoundary>
);

const App = () => (
  <ReDotProvider
    config={{
      providers: {
        [SOME_GENESIS_HASH]: new WsProvider("wss://my.chain"),
      },
    }}
  >
    <ReDotChainProvider genesisHash={SOME_GENESIS_HASH}>
      <Balance />
    </ReDotChainProvider>
  </ReDotProvider>
);

Timeline & Budget

Requested amount: 6,000 DOT

Estimated length of work: 8 weeks/~320 hours

Estimated rate: 18.75 DOT or ~139.20 USD per hour

The requested amount also covers the retrospective work from numerous experiments and research efforts that validated this idea and led to the development of the initial working proof of concept.

Planned schedule

  1. Week 1: Research & planning
  2. Week 2-4: Core development
  3. Week 5-6: Writing unit & integration tests
  4. Week 7: Documentation, walkthrough & website
  5. Week 8: Official version 1
  6. Ongoing: Feedback & iteration

Of which version 1 will include React support for the capabilities outlined in the section before, excluding possible future goals

Conclusion

reactive-dot aims to revolutionize the way developers interact with the Polkadot network by providing a robust, user-friendly, and feature-rich React library. By simplifying the development process and fostering a vibrant community, reactive-dot will play a pivotal role in promoting the adoption and growth of the Polkadot ecosystem. We seek the support and funding from the treasury to bring this ambitious project to life.

Up
Comments
No comments here