How to Expose an SDK with React’s useContext
Breaking down Thirdweb's React SDK
Introduction
I recently built a simple NFT Marketplace leveraging Thirdweb, and I was awed by how clean and simple it was to use their React SDK.
Having used their platform to ship a marketplace contract, the only front end code I needed to interact with my contract was this:
import { useActiveListings, useMarketplace } from "@thirdweb-dev/react";
const Home: NextPage = () => {
const marketplace = useMarketplace(<"marketplace contract address">);
const { data, isLoading } = useActiveListings(marketplace);
...
// render data in some meaningful way
}
Caveat: one additional step was required, wrapping my root nextJS component in a ThirdwebProvider
like so:
import { ChainId } from "@thirdweb-dev/sdk";
import { ThirdwebProvider } from "@thirdweb-dev/react";
function MyApp({ Component, pageProps }: AppProps) {
return (
<ThirdwebProvider desiredChainId={ChainId.Goerli}>
<Component {...pageProps} />
</ThirdwebProvider>
);
}
Naturally, I wanted to learn more, so I poked around their SDK repository and tried to learn what best practices I could.
It turns out that the Provider
delivers most of the magic for us, and is what enables us to hook into an SDK with a single function like useMarketplace
.
This type of pattern leverages Reacts Context feature. Let's break down exactly how this works, and then explore how Thirdweb leverages it to create such a clean access pattern.
What is React Context
Context is useful for setting high-level properties without needing to "props drill" and pass information down several layers of React components. Instead, the context is defined in the top-most component, and can be read with React's useContext
.
Context is NOT built for dynamic state management. It is best suited for values which will be relatively static, such as:
- dark / light themes
- user data
- location specific data
And for decentralized apps interacting with blockchains, context is commonly used to expose:
- chain ID
- node provider
- connected wallet and signer
It turns out context is a fabulous way to handle web3 related data, so it will be a valuable pattern to learn.
How to Use React Context
To use this React feature in your app, you have to complete a few steps:
- Create the context with
createContext
- Wrap a context provider around consuming components
- Inject values into context
- Read value from context with
useContext
I go into detail below, but you can reference the codesandbox example here.
1. Create the context with createContext
Create a context variable, named however you see fit (in this case, we are storing user information).
import * as React from "react";
export const UserContext = React.createContext();
(pages/index.tsx
in the sandbox)
2. Wrap a context provider around consuming components
Wrap the root component in a context provider. Any child Components will be able to read from the context. Ignore the UserDisplay
for a second, it becomes relevant in step 4.
import * as React from "react";
import UserDisplay from "../components/user";
export const UserContext = React.createContext();
export default function IndexPage() {
return (
<div>
<UserContext.Provider value={"test"}>
<h1 className="text-sm">
Hello World
<br />
</h1>
<UserDisplay />
</UserContext.Provider>
</div>
);
}
(pages/index.tsx
in the sandbox)
3. Inject values into context
Now we can instantiate a value, and even program a stateful toggle. For this example, we are just hardcoding a value.
import * as React from "react";
import UserDisplay from "../components/user";
export const UserContext = React.createContext();
export default function IndexPage() {
const contextValue = { name: "Reed", id: "123" };
return (
<div>
<UserContext.Provider value={contextValue}>
<h1 className="text-sm">
Hello World
<br />
</h1>
<UserDisplay />
</UserContext.Provider>
</div>
);
}
(pages/index.tsx
in the sandbox)
4. Read value from context with useContext
Finally, we can read the value from the context, without passing the information down as props! Inside the UserDisplay
component, we can do this:
import * as React from "react";
import { UserContext } from "../pages/index";
export default function UserDisplay() {
const value = React.useContext(UserContext);
return (
<div className="text-xl">
We are reading this from the context:
<br />
<span className="text-blue-500">name: {value.name}</span>
<br />
<span className="text-blue-500">id: {value.id}</span>
</div>
);
}
(components/user.tsx
in the sandbox)
Giving us the below:
While this is a simple example, we can leverage this to do some robust functionality.
How Thirdweb Exposes Their SDK with React Context
For some background, Thirdweb offers a thirdweb-dev/sdk
library, which can be imported, and used like below:
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
const sdk = new ThirdwebSDK("polygon");
// instantiate code path to your contract
const marketplace = sdk.getMarketplace("<CONTRACT_ADDRESS>");
const nftDrop = sdk.getNFTDrop("<CONTRACT_ADDRESS>");
// read from your contract
const listings = await marketplace.getActiveListings();
const claimedNFTs = await nftDrop.getAllClaimed();
However, if we start introducing multiple React components which all require different functions from the SDK (marketplace / NFTDrop / etc), we start to move away from DRY code (don’t repeat yourself!). Re-defining the SDK in each file requires duplicate lines, and also requires SDK arguments (like targetChain=”polygon”) to be specified in each instantiation.
While one method of avoiding this is to instantiate the SDK with its configuration in one place, and import it in each file, it makes it much less friendly and consumable as a library.
The method Thirdweb used to make their SDK super accessible is the pattern I mentioned above, using React’s useContext.
If we poke around their thirdweb-dev/react
repository, they instantiate a context object, as well as a ThirdwebSDKProvider
which instantiates their SDK, and plugs it into the context (see ctxValue
):
This enables a useSDK pattern, and eliminates the need to instantiate the ThirdwebSDK object every time you want to execute a function on it.
export function useSDK(): ThirdwebSDK | undefined {
const ctx = React.useContext(ThirdwebSDKContext);
invariant(
ctx._inProvider,
"useSDK must be called from within a ThirdwebProvider, did you forget to wrap your app in a <ThirdwebProvider />?",
);
return ctx.sdk;
}
And finally, to expose a clean, minimal setup hook like useMarketplace
, they do the below:
Now, their context contains their SDK, and accessing functions is as simple as returning sdk.marketplace
.
Here are the links to useMarketplace
and useBuiltInContract
if you want to take a look.
Conclusion
I seriously appreciate this pattern. It makes the SDK access patterns incredibly clean.
While context may be overkill for small projects, it's valuable for libraries which will be consumed by other codebases, and it supports scaling up and refactoring much better, since there are minimal repeated imports and we can avoid “props drilling”.
I highly recommend checking out this pattern and trying to implement it in your own project, especially if you expect to scale up or you find yourself props drilling.