How to Expose an SDK with React’s useContext

How to Expose an SDK with React’s useContext

Breaking down Thirdweb's React SDK

Featured on Hashnode

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:

  1. Create the context with createContext
  2. Wrap a context provider around consuming components
  3. Inject values into context
  4. Read value from context withuseContext

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: Screen Shot 2022-08-11 at 10.32.32 PM.png

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):

Screen Shot 2022-08-08 at 12.17.12 PM.png

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:

sc3.png

sc2.png

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.