Building a Project with SE-2 | Crowd Fund | Part Seven | The Components

WebSculpt
7 min readFeb 16, 2024

--

Image from Shubham Dhage on Unsplash

If you are here for the first time, then you may want to start at the beginning of this series.
Other posts in the series:
V1’s Smart Contract
V1’s Frontend Components
V2’s Smart Contract
V2’s Frontend Components
V3’s Subgraph

You are currently on the article for V3’s frontend components (View code on GitHub).
View Live Site

The first component we will go over is FundRunsList. You can see it in action on this Live Page …

☝️comes from this component

This used to be a list of Fund Runs that our Smart Contract returned, but now we are querying this data from a subgraph. So, what does this change about the code? Let’s discuss the client that our frontend will be using to query this data.

Apollo Client

We will be utilizing Apollo Client — which is used to fetch, cache, and modify data with GraphQL.

Apollo Client is configured on our \pages\_app.tsx page:

 
import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client";
..........
const apolloClient = new ApolloClient({
uri: subgraphUri,
cache: new InMemoryCache(),
});
........
return (
<ApolloProvider client={apolloClient}>
<WagmiConfig config={wagmiConfig}>
<NextNProgress />
<RainbowKitProvider
chains={appChains.chains}
avatar={BlockieAvatar}
theme={isDarkTheme ? darkTheme() : lightTheme()}
>
<div className="flex flex-col min-h-screen">
<Header />
<main className="relative flex flex-col flex-1">
<Component {...pageProps} />
</main>
<Footer />
</div>
<Toaster />
</RainbowKitProvider>
</WagmiConfig>
</ApolloProvider>
);

☝️Configure your Apollo Client (more options here), and then wrap an <ApolloProvider /> around everything!

A Fund Run Query

Let’s go back to the FundRunsList component

In the imports of this component, you will see that the query ( GQL_FUNDRUNS_For_Display ) is in this file. To make things simple, all of the queries are in that file.

useQuery

Apollo offers useQuery (a React hook) as the primary API for executing queries.
Apollo’s example of useQuery 👇

import { gql, useQuery } from '@apollo/client';

const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
}
}
`;
............
.........
......
function Dogs({ onDogSelected }) {
const { loading, error, data } = useQuery(GET_DOGS);

if (loading) return 'Loading...';
if (error) return `Error! ${error.message}`;

return (
<select name='dog' onChange={onDogSelected}>
{data.dogs.map((dog) => (
<option key={dog.id} value={dog.breed}>
{dog.breed}
</option>
))}
</select>
);
}

But, in our app, we want to offer paging as well as an option for the user to change how many Fund Runs are on a page 👇

When we are querying, we can state that we want to only take X amount of items, as well as how many times to “skip. We are going to send our query these two Ints, via the variables: $limit and $offset:
query ($limit: Int!, $offset: Int!) { ... }

Our Fund Runs List will be limited to our specific query here:
fundRuns(first: $limit, skip: $offset) { ... }

We are also going to Order-By the Fund Run’s ID (the ID from the contract starts at 0 and increments up), so the query looks more like this:
(orderBy: fundRunId, orderDirection: desc, first: $limit, skip: $offset)

The whole query 👇

//for viewing the Fund Runs list on /browse-fund-runs
//returns latest-first
export const GQL_FUNDRUNS_For_Display = () => {
return gql`
query ($limit: Int!, $offset: Int!) {
fundRuns(orderBy: fundRunId, orderDirection: desc, first: $limit, skip: $offset) {
id
fundRunId
owners
title
description
deadline
target
amountCollected
amountWithdrawn
status
}
}
`;
};

Then, we use useQuery like this:

const { loading, error, data } = useQuery(GQL_FUNDRUNS_For_Display(), {
variables: {
limit: pageSize,
offset: pageNum * pageSize,
},
pollInterval: 5000,
});

☝️take note of how the limit and offset variables are passed, along with a polling interval.

Similar to before, we then map each item (Fund Run) that gets returned to a FundRunDisplay component.

Proposals Table

If you look into a Fund Run that has multiple owners, you will see the Proposals Table 👇

The ProposalsTable Component is also querying with useQuery, check it out 👇

const { loading, error, data } = useQuery(GQL_PROPOSALS_By_FundRunId(), {
variables: { slug: frVault.fundRunId },
pollInterval: 1000,
});

Here is the query:

//for the table on Vaults page
//will not return revoked proposals
export const GQL_PROPOSALS_By_FundRunId = () => {
return gql`
query ($slug: Int!) {
proposals(where: { fundRunId: $slug, status_lt: 3 }) {
id
proposalId
fundRunId
proposedBy
amount
to
reason
status
signatures {
id
signer
signature
}
}
}
`;
};

☝️This query is only returning Proposals with the correct Fund Run ID.
proposals(where: { fundRunId: $slug, …

When querying, you can also use suffixes like _lt (less-than), _gt (greater-than), and _lte (less-than-equal-to) for value comparisons:

We have an example of this in our own app.
Below, we’re basically saying that we only want to return Proposals that have a status of less than 3 👇
proposals(where: { fundRunId: $slug, status_lt: 3 })

More information: Logical Operators on Graph API

The Nested Table

The Proposals Table component maps the (queried) single Proposals ( inside of a <tbody /> ) to a Proposal Row component, which has TWO table-rows:

Filtering

Some of the queries offer “Search” abilities (filtering):

A table that can be filtered by Wallet Address

I want to tell you about a “bug” I ran into

Maybe this will save someone else some time along the way. When I first set out to offer the ability to filter a query real-time — I was under the impression that I should be using the useLazyQuery hook.
The problem I ran into was that useLazyQuery gets executed if ANY of the options change (you can read more about it here). This isn’t really a “bug”, but a lot of people end up using this hook the wrong way.

I want my table — however — to load initially, and then it should get filtered if the user types anything into the search bar.
So, instead of useLazyQuery, I just used the Apollo Client directly:

const client = useApolloClient();
.........
await client.query({
query: GQL_PROPOSALS_Snapshot(readiedSearchInput),
variables: {
limit: pageSize,
offset: pageNum * pageSize,
searchBy: readiedSearchInput,
},
fetchPolicy: "no-cache",
})

GQL_PROPOSALS_Snapshot returns slightly different queries depending on whether or not a Search Input is supplied.
IF supplied, the query will search BOTH proposedBy AND to fields to see if either contain the Contract Address you are searching for:
where: { or: [{ proposedBy_contains: $searchBy }, { to_contains: $searchBy }] }
☝️ This is ‘WHEREproposedBy contains;ORto contains the search-by string.

return gql`
query ($limit: Int!, $offset: Int!, $searchBy: String) {
proposals(
where: { or: [{ proposedBy_contains: $searchBy }, { to_contains: $searchBy }] }
orderBy: proposalId
orderDirection: desc
first: $limit
skip: $offset
) {
id
proposedBy
amount
to
reason
status
fundRun {
id
title
amountCollected
amountWithdrawn
}
}
}
`;

Nested-Entity Filtering

I have an example of nested-entity filtering on the Latest Signers Table.

We will now filter results from the top-level as well as the nested-level.
How?

The table you are looking at is of Proposal Signatures, but every one of them has a related Proposal.
The Signer belongs to a Proposal Signature.
The To belongs to a Proposal.

We want to search both of those fields for a Wallet Address.
Here’s the query to do that:

return gql`
query ($limit: Int!, $offset: Int!, $searchBy: String) {
proposalSignatures(
where: { or: [{ signer_contains: $searchBy }, { proposal_: { to_contains: $searchBy } }] }
orderBy: proposalId
orderDirection: desc
first: $limit
skip: $offset
) {
id
proposalId
signer
proposal {
id
amount
to
reason
}
}
}
`;

The specific code:
where: { or: [{ signer_contains: $searchBy }, { proposal_: { to_contains: $searchBy } }] }
☝️ This is ‘WHEREProposal Signature signer contains;ORProposal to contains the search-by string.
The “nested” bit is:
, { proposal_: { to_contains: $searchBy } }] }
☝️ specifying to search the Proposal for to

--

--

WebSculpt

Blockchain Development, coding on Ethereum. Condensed notes for learning to code in Solidity faster.