-
Notifications
You must be signed in to change notification settings - Fork 9
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
base: main
Are you sure you want to change the base?
feat(counter): Add builders check-in count implementation #18
Conversation
@All-Khwarizmi is attempting to deploy a commit to the BuidlGuidl Team on Vercel. A member of the Team first needs to authorize it. |
The latest updates on your projects. Learn more about Vercel for Git ↗︎
|
There was a problem hiding this 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"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 /> |
There was a problem hiding this comment.
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"; |
There was a problem hiding this comment.
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
ba032b9
to
9fb74d4
Compare
SSR.demo.movThanks for the detailed feedback! I'd like to share the implementation journey as it demonstrates some interesting Next.js patterns:
// 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:
The final implementation uses viem's 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! |
Description
Implemented builders check-in counter with two possible ABI handling approaches:
Current Implementation:
Alternative Approach (from ContractVariables.tsx):
Implementation Considerations:
Which approach would be preferred in this context?
Additional Information
Related Issues
Closes #8
Your ENS/address: 0x7d971C39bb700AcEc20879D46f092dC0DB1dbF9E