NOTE: Polkassembly doesn't do code highlighting, so a much easier to read version can be found here instead.
reactive-dot
: A reactive library for building Substrate front-endI'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.
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",
});
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 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>
);
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>
);
The library aim to provides strong TypeScript definition with 1-1 mapping to Substrate pallets definition.
The scope of this library can expand significantly based on community interest. Potential future features include:
U32
-> Number
, U256
-> BigInt
, etc)A working proof of concept showcasing the library can be found here.
The below code snippets perform the following tasks:
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>
);
};
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>
);
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.
Of which version 1 will include React support for the capabilities outlined in the section before, excluding possible future goals
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.