Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(counter): Add builders check-in count implementation #18

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

All-Khwarizmi
Copy link
Contributor

Description

Implemented builders check-in counter with two possible ABI handling approaches:

Current Implementation:

// Direct ABI definition approach
const BatchRegistryABI = {
  checkedInCounter: {
    name: "checkedInCounter",
    type: "function",
    stateMutability: "view",
    inputs: [],
    outputs: [{ type: "uint256" }],
  },
} as const;

Alternative Approach (from ContractVariables.tsx):

// Dynamic ABI filtering approach
const abiFunction = (deployedContractData?.abi as Abi)?.filter(
  part => part.type === "function" && part.name === TARGET_FUNCTION_NAME,
)[0] as AbiFunction;

Implementation Considerations:

  • Direct ABI is more performant (no runtime filtering for only one function) and more readable
  • Dynamic approach is more flexible for contract updates
  • Both maintain type safety

Which approach would be preferred in this context?

Additional Information

  • Following issue requirements
  • Added inline documentation explaining both approaches
  • Added comparative analysis in comments

Related Issues

Closes #8

Your ENS/address: 0x7d971C39bb700AcEc20879D46f092dC0DB1dbF9E

Copy link

vercel bot commented Jan 8, 2025

@All-Khwarizmi is attempting to deploy a commit to the BuidlGuidl Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

vercel bot commented Jan 8, 2025

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
batch12.buidlguidl.com ✅ Ready (Inspect) Visit Preview 💬 Add feedback Jan 8, 2025 6:43pm

Copy link
Member

@derrekcoleman derrekcoleman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR, All-Khwaizmi! Please take a look at my comments for some specific feedback. As to your question,

Implementation Considerations:

  • Direct ABI is more performant (no runtime filtering for only one function) and more readable
  • Dynamic approach is more flexible for contract updates
  • Both maintain type safety
    Which approach would be preferred in this context?

I personally think readability (specifically, maintainability) is quite important for helping us reason through what our code does, something mostly lost in the second option you offer. The fact that the first is more performant is icing on the cake (and potentially irrelevant if you're able to do SSR, as I suggest).

Lastly, I think it's a great habit to include a screenshot of your changes in the PR's description. Please update your original message with a preview of the page with the counter component.

Let us know if you have any questions!

@@ -0,0 +1,62 @@
"use client";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible to render this server-side? It's a query for an integer that we render in a component that has no interactivity, so I would think it's a good candidate for SSR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went down the rabbit hole of SSR wrt to our case and smart contracts in general and here are my findings:

We have different options in hand to SSR this section:

  • For starters we can remove the use client directive from the home page following the same reasoning.
  • For the BuildersCheckInCount since we're using hooks we can't as is.
  • We could try the pattern composition and have a CounterDisplay (child) server component to render the counter but it might be an overkill for not much since the parent remains client side anyway.
  • So why not try to query the counter from the server already using viem's create public client. To do so we would need to remove the hooks, keep the ABI definition and have access to the contract address either programmatically (how?) or hardcoded on last resort.
// BuildersCheckInCount.tsx (Server Component)
import { createPublicClient, http } from 'viem'
import { optimism } from 'viem/chains'

const CONTRACT_ADDRESS = '0x...'

async function BuildersCheckInCount() {
  // Create public client
  const client = createPublicClient({
    chain: optimism,
    transport: http()
  })

  try {
    // Read from contract
    const counter = await client.readContract({
      address: CONTRACT_ADDRESS,
      abi: ABI,
      functionName: FUNCTION_NAME,
    })
...
  • Second best approach in my opinion is the suspense one, where we SSR the home page and stream the child client component using the suggested useScaffoldReadContract hook.
// In home page
  <Suspense fallback={<LoadingCounter />}>
            <BuildersCheckInCount />
 </Suspense>

// BuildersCheckInCount
import { useScaffoldReadContract } from "~~/hooks/scaffold-eth";

function BuildersCheckInCount() {
  // Get the result of the target function call from the deployed contract
  const {
    data: counter,
    isLoading,
    error,
  } = useScaffoldReadContract({
    contractName: CONTRACT_NAME,
    functionName: FUNCTION_NAME,
  });

  return (
    <div className="text-lg flex gap-2 justify-center items-center">
      <span className="font-bold">Checked in builders count:</span>
      {isLoading || (!error && !counter) ? (
        <ArrowPathIcon className="size-5 animate-spin" />
      ) : error ? (
        <span className="text-error">Error!</span>
      ) : (
        <span>{displayTxResult(counter)}</span>
      )}
    </div>
  );
}

Notice the isLoading || (!error && !counter) trick to avoid a clunky initial state without any data nor loading state

Without the trick

Enregistrement.de.l.ecran.2025-01-10.a.07.58.50.mov

With it

Enregistrement.de.l.ecran.2025-01-10.a.07.57.29.mov

What do you think is the best approach?
Is there other options that I did not consider?

Copy link
Member

@derrekcoleman derrekcoleman Jan 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For starters we can remove the use client directive from the home page following the same reasoning.

Yup, love it!

We could try the pattern composition and have a CounterDisplay (child) server component to render the counter but it might be an overkill for not much since the parent remains client side anyway.

Even though the parent is client side, it renders quite quickly because it has no fetching to do. In other words, only making the CounterDisplay server-side (to save on its loading time) would still accomplish the goal pretty well without much scope creep.

So why not try to query the counter from the server already using viem's create public client. To do so we would need to remove the hooks, keep the ABI definition and have access to the contract address either programmatically (how?) or hardcoded on last resort.

If you're interested in practicing this implementation, this would be a great exercise. In my opinion hardcoding the contract address is fine, since our context is a specific batch with a static contract address; we don't expect it to change for this repo.

Second best approach in my opinion is the suspense one, where we SSR the home page and stream the child client component using the suggested useScaffoldReadContract hook.

A fantastic pattern, definitely better than having no loading state then having layout shift as the data pops in (especially for larger streaming components, such as YouTube's homepage). If you don't want to go the viem route, this kind of approach is good enough.

What do you think, @All-Khwarizmi?

<span className="font-bold">Checked in builders count:</span>
<span>To Be Implemented</span>
</p>
<BuildersCheckInCount />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I appreciate the way you factored out the logic to keep things organized 👌


import React from "react";
import { displayTxResult } from "../debug/_components/contract";
import { useReadContract } from "wagmi";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you know about the useScaffoldReadContract hook (docs)? I encourage you to try a quick refactor using it so you get some experience with one of Scaffold-ETH's most commonly used tools.

- Add BuildersCheckInCount component with simplified ABI usage
- Document ABI handling approaches with reference to existing patterns
- Add comparison with ContractVariables implementation
- Implement direct ABI definition for better performance
- Use useScaffoldReadContract to simplify the counter query to its simplest form.
- Removed the ABI manual implementation and the usage of wagmi hook to interact with smart contract.
- Remove `use client` directive.
- Use `suspense` boundary to stream the BuildersCheckInCount client component.
- Add LoadingCounter component for loading state.
- Refactor BuildersCheckInCount to render a child server component. This bend the pattern composition rule but works thanks to the fact that all the child component are serializable. (This would be explained more in the PR)
- Add CounterDisplay server component wich is rendered in the server, sent to the client and the rehydrated with the current values when the become available in the client component.
- Remove client-side hooks in favor of viem's createPublicClient
- Implement contract reading directly on server
- Eliminate loading states thanks to SSR
- Reduce client-side JavaScript bundle
@All-Khwarizmi All-Khwarizmi force-pushed the feat/checkin-counter-v2 branch from ba032b9 to 9fb74d4 Compare January 11, 2025 10:34
@All-Khwarizmi
Copy link
Contributor Author

SSR.demo.mov

Thanks for the detailed feedback! I'd like to share the implementation journey as it demonstrates some interesting Next.js patterns:

  1. Initial Streaming Implementation with Serialization
    I had an intermediary implementation that used Suspense boundaries to stream a client component that in turn rendered a server component:
// Home.tsx (Server Component)
<Suspense fallback={<LoadingCounter />}>
  <BuildersCheckInCount /> {/* Client Component */}
</Suspense>

// BuildersCheckInCount.tsx (Client Component)
"use client"
import { CounterDisplay } from "./CounterDisplay"; // Server Component!

function BuildersCheckInCount() {
  const { data: counter, isLoading, error } = useScaffoldReadContract({...});
  return <CounterDisplay data={counter} isLoading={isLoading} error={error} />;
}

This pattern technically bends Next.js rules by importing a Server Component into a Client Component, but works because:

  • All props (counter, isLoading, error) are serializable
  • The data flow is unidirectional (client → server)
  • The Server Component is pre-rendered and its result is serialized for the client. Then when the data is available the component is hydrated.

Note: if you remove the suspense boundary the app would break!

  1. Final SSR Implementation
    Given these observations, I opted for full SSR which offers several advantages:
  • No loading states needed - better UX
  • Lighter client bundle - better for mobile
  • Simpler mental model - no serialization tricks
  • Better performance as demonstrated in the video

I had to remove some utility functions (like bigint formatting) that weren't serializable, highlighting the constraints of this pattern.

The final implementation uses viem's createPublicClient directly on the server:

async function BuildersCheckInCount() {
  const publicClient = createPublicClient({...});
  const counter = await publicClient.readContract({...});
  return <CounterDisplay data={counter} />;
}

Rather than trying to bend the Next.js recommendations (which might quick turn into an anti-pattern), the full SSR approach provides better performance and maintainability while keeping the code straightforward.

Let me know if this reasoning makes sense or if you see other patterns worth exploring!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5. Get the check-in count from the BatchRegistry contract
2 participants