owned this note
owned this note
Published
Linked with GitHub
**NOTE**: Polkassembly doesn't do code highlighting, so a much easier to read version can be found [here](https://hackmd.io/wUbQpWBqTFaPD-I6D_O6Mw?view) instead.
# Proposal for `reactive-dot`: A reactive library for building Substrate front-end
## Overview
I'm [Tien](https://github.com/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.
```tsx
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
```ts
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.
```ts
// 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.
```tsx
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.
```tsx
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](https://hackmd.io/_uploads/BkVFkEe4A.png)
![image](https://hackmd.io/_uploads/rkIo14lNC.png)
![image](https://hackmd.io/_uploads/SyeXA1VeNR.png)
#### Strong return type definition
![image](https://hackmd.io/_uploads/ByfE1Vl4R.png)
![image](https://hackmd.io/_uploads/HkXWJNe4A.png)
### 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](https://stackblitz.com/edit/vitejs-vite-lyatc5?file=src%2FApp.tsx).
## 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
```tsx
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`
```tsx
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](#key-features-for-the-first-version) before, excluding possible [future goals](#and-more)
## 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.