These are Reis’ thoughts on frontend architecture, organization, and conventions.
The frontend UI should be a function of state, which is derived primarily from a backend/database source. This gets tricky when we’re developing “reactive” UIs, where we mutate and query state without refreshing the page.
React JSX components should be simplified as much as possible. The ideal component is primarily communicating HTML, CSS, and layout. We’re injecting state as simply and idiomatically as possible. Other concerns should be abstracted out and named to clearly communicate what is happening. That said, sometimes it’s clearest to do logic operations in the component. Good examples of this are opening/closing a modal, incrementing a page value, etc.
Hooks are good for creating abstractions that deal with React state. It’s worth knowing the Rules of Hooks. Hooks can use other hooks (useState, useMemo, etc.). If you’re creating an abstraction that does NOT deal with state, it might not need to be a hook. Perhaps it’s more appropriately a utility or service?
Hooks should describe a narrow scope of functionality. Ideally, the hook is just dealing with one data point or set of data points (ie. job or jobs.)
const { data, refetch, isLoading, error } = useJob(id: JobID)
useJobs()
useWallet()
useCreateJob
useJobs
after the service call completes. Arguably, we could include this method in useJobs
, where it would auto-refetch jobs after being called - but it would be fine to just keep it as a separate service too.Option 1:
const { jobs, refetch } = useJobs();
await ContractsService.createJob(job);
refetch();
return <div>{jobs.map(job => <p>{job.title}</p>)}</div>
Option 2:
const { jobs, createJob } = useJobs();
createJob(job);
return <div>{jobs.map(job => <p>{job.title}</p>)}</div>
Of course, we could make it work in a hook, but to me it’s less clear that way. We’re essentially doing an imperative call to the contract (a service). Jobs, however, are state that we interact with in the application and want to provide across many components.
useCalculateBuyIn
Services are interfaces with other domains. These might be internal or external - usually external. So for example, communicating with ethers
would ideally be encapsulated in a service. That way, the “contract communication” domain can be designed and scoped to our needs without exposing things we don’t need. HTTP calls, IFPS, Firebase, and Metamask are other examples of good candidates to go in src/services
.
// src/services/ipfs.ts
import ipfs from 'ipfs';
const getIPFSData = (id: string) => {
return ipfs.getData(id).filter(item => item.consumer === 'engineerdao');
}
export { getIPFSData };
Utilities are functions that have broad use. I usually categorize them in primitive categories like strings
or numbers
. They should be pure functions. They should have no knowledge of application state. If a utility is looking very specific to some function of the application, consider re-classifying as a service or even a component-level utility. Sometimes I’ll throw a component.utils.ts
next to my components if I find that there are too many functions in the main component file, but they’re very specific to that component.