This directory contains a single-page React app that displays a decentralized Forum, pulling forum state from a blockchain smart contract and post and comment content from IPFS and Filecoin.
See the main repository readme for instructions on running in local development mode from the repo root. This will also start the local development blockchain, deploy the contract, and write the deployment information into ./src/contracts/hardhat_contracts.json
in this package.
If you have a reason to run this package independently, you can install the dependencies with npm install
and run npm run dev
in this directory.
Important: the app will fail to launch unless the file ./src/contracts/hardhat_contracts.json
exists. This is created by the npm run deploy
action in the contract package, which will run automatically when running npm run dev
from the repo root. If you've customized the deployment setup, you may need to provide this file yourself.
Broadly speaking, the code is organized into three main areas of responsibility.
./src/chain
contains code related to connecting to the deployed smart contract using an Ethereum provider. See Ethereum interactions below for details../src/api
contains a high-level forum API for interacting with the Forum. This wraps the "raw" smart contract API to hide some of the Ethereum-specific details, and it also integrates off-chain storage of post and comment content using the Web3.Storage client../src/components
contains the user interface components, routing logic, and other "front end" things.
The app uses the ethers.js library for connecting to and interacting with the smart contract.
To integrate with the React lifecycle, the web3-react library provides a Context-based API that can integrate with browser wallets like MetaMask, as well as JSON-RPC based nodes.
To provide a better experience for logged out users, the app uses two Ethereum providers. The "read-only" provider uses a JSON-RPC connection to a remote node (or local development node). This provider is used for all read-only contract calls, since they don't require payment or any user interaction.
To log in to the app, the user connects their MetaMask wallet, which enables web3-react's "injected" provider. The ForumAPI
uses this "authorized" provider to connect to the contract in read/write mode.
These two providers are managed in ./src/chain/context.tsx
, which defines a custom context called ChainContext
:
interface ChainContextInterface {
readonly: Web3ReactContextInterface<Web3Provider>,
authorized: Web3ReactContextInterface<Web3Provider>,
loggedIn: boolean,
account: string | null | undefined,
readonlyContract: ForumContract | undefined,
authorizedContract: ForumContract | undefined,
}
This can be used from any React component with the useChainContext
hook. Most components will likely only need the loggedIn
and account
context fields. The readonlyContract
and authorizedContract
fields are consumed by another custom Context provider, described below in the Forum API section below.
The ForumAPI
class defined in ./src/api/forum.ts
provides a high-level interface to the smart contract and off-chain decentralized storage.
The ForumAPI
uses the readonlyContract
and authorizedContract
defined in the ChainContext
described above. The read only contract is required in order to construct a ForumAPI
instance, but you can omit the authorizedContract
if the user hasn't logged in yet. If there's no authorizedContract
, attempting to vote or submit posts or comments will result in an error.
Submitting a post or comment will store the content of the item using Web3.Storage, making it available on IPFS with long-term backing by Filecoin. Once the content has been stored, the Content Identifier (CID) is stored on-chain using the addPost
or addComment
smart contract functions.
In practice, you probably won't need to create a ForumAPI
instance yourself. Instead, use the useApiContext
hook defined in ./src/api/context.tsx
:
function MyComponent() {
const { api } = useApiContext()
if (!api) {
// api will be undefined until the read-only connection to the contract is established
return <LoadingSpinner />
}
if (api.canPost) {
// show post ui...
}
}
The useApiContext
hook will update when the ChainContext
changes. Before the read-only connection is made, api
will be undefined, so make sure to check before using it! You should also check the canVote
and canPost
properties before invoking the vote or post methods. These will be updated when the user connects their wallet.
To avoid hard-coding an API token into the front end, the dApp requires each user to provide their own Web3.Storage API token before they can submit posts or comments. Attempting to post without a token will redirect to the /account
page defined by the AccountSettings
component. Adding a token in the settings page will persist it to the browser's localStorage
.
If an API token is avaliable in localStorage
, the ForumAPI
instance returned by the useApiContext
hook will support posting, and api.canPost
will be true
.
Note that requiring the user to supply their own key will not be necessary once the Web3.Storage service supports public key authentication, as described in this issue. This example will be updated to support public key authentication once that functionality has been implemented.
The UI code lives in ./src/components
. The App
component is the main "entry point". It defines the routes using React Router and wires up the various context providers.
The main feed UI component is the PostList
, which renders an ItemSummary
that links to an ItemDetails
page for each post.
The Header
component shows some details about the logged-in account, or a login link if no wallet is connected.
Interactions with the API are managed using react-query, which provides a unified querying layer with built-in caching for any asynchronous data source. Query keys and hooks for "mutations" are defined in ./src/api/queries.ts
.
See the React Query docs if you're new to React Query. The Practical React Query blog is another good resource, particularly Effective React Query Keys, which informed the design of the query keys in this app.