Skip to content
This repository was archived by the owner on May 22, 2025. It is now read-only.

Connect for React API#52

Merged
bpierre merged 47 commits intomasterfrom
connect-react
Jul 5, 2020
Merged

Connect for React API#52
bpierre merged 47 commits intomasterfrom
connect-react

Conversation

@bpierre
Copy link
Copy Markdown
Contributor

@bpierre bpierre commented Jun 18, 2020

Builds on #111

Summary

This PR adds an initial version of the React library.

<Connect />

A decision was made to move the configuration on the <Connect /> component, which makes it possible to declare it once in the app. The idea is to optimize for the use case of connecting to a single organization, and interacting with it. Users can still declare <Connect /> in different places to connect to more than one organization, or use @aragon/connect directly.

Hooks for subscriptions only

The only hooks provided are abstracting the subscriptions. Async calls are left as promises for now. This is something we might want to reconsider in the future depending on usage.

@aragon/connect re-exports

The entire @aragon/connect is being exported by the React library. This is to make it easier when adding new types, but we might prefer to list the exports instead.

App connectors

There is no hook allowing to subscribe to apps through their connectors yet. The plan is to do it as a second step, once they can be instantiated from the connection in the core library.

Organization

The Organization object has been left as it is, including methods that are not needed with the react library like .apps(). This is to avoid having different types for the React library and for the others.


# Using Aragon Connect with React

Introduction

Aragon Connect provides a series of utilities that makes it easier to use in React environments.

It consists of the <Connect /> component, on which a connection to an organization is described, and a series of hooks: useApp(), useApps(), useOrganization(), usePermissions().

Usage

import {
  Connect,
  useApps,
  useOrganization,
  usePermissions,
} from '@aragon/connect-react'

function App() {
  const [org, orgStatus] = useOrganization()

  const [apps, appsStatus] = useApps()
  const [permissions, permissionsStatus] = usePermissions()

  const loading =
    orgStatus.loading || appsStatus.loading || permissionsStatus.loading
  const error = orgStatus.error || appsStatus.error || permissionsStatus.error

  if (loading) {
    return <p>Loading…</p>
  }

  if (error) {
    return <p>Error: {error.message}</p>
  }

  return (
    <>
      <h1>{org.name}</h1>

      <h2>Apps</h2>
      <ul>
        {apps.map((app, i) => (
          <li key={i}>{app.name}</li>
        ))}
      </ul>

      <h2>Permissions</h2>
      <ul>
        {permissions.map((permission, i) => (
          <li key={i}>{String(permission)}</li>
        ))}
      </ul>
    </>
  )
}

ReactDOM.render(
  <Connect location="myorg.aragonid.eth" connector="thegraph">
    <App />
  </Connect>,
  document.querySelector('main')
)

API

<Connect />

This component is required in order to use the provided hooks.

Props Type Description
location String The Ethereum address or ENS domain of an Aragon organization.
connector Connector | [String, Object] | String Accepts a Connector instance, and either a string or a tuple for embedded connectors and their config.
options Object The optional configuration object.
options.readProvider EthereumProvider An EIP-1193 compatible object.
options.chainId Number The Chain ID to connect to. Defaults to 1.

useOrganization()

Props Type Description
returns [Organization | null, { loading: boolean, error: null | Error, retry: Function }] An array containing the organization and a loading status object.

useApp(appFilters)

Name Type Description
appFilter String or object (optional) When a string is passed, the app will get searched by address if it starts by 0x, and by appName otherwise. See appFilter.address and appFilter.appName to set them explicitly. For the time being, only one type of filter can get passed at a time.
appFilter.address String Same as appFilter, but makes the selection by address explicit.
appFilter.appName String Same as appFilter, but makes the selection by appName explicit.
returns [App | null, { loading: boolean, error: null | Error, retry: Function }] An array containing a single app from the organization and a loading status object.

useApps(appFilters)

Name Type Description
appFilter String or String[] or object (optional) When a string is passed, apps will get filtered by address if it starts by 0x, and by appName otherwise. When an array is passed, the first entry determines the type of filter. See appFilter.address and appFilter.appName to set them explicitly. For the time being, only one type of filter can get passed at a time.
appFilter.address String or String[] Same as appFilter, but makes the selection by address explicit.
appFilter.appName String or String[] Same as appFilter, but makes the selection by appName explicit.
returns [App[], { loading: boolean, error: null | Error, retry: Function }] An array containing the organization apps and a loading status object.

usePermissions()

Name Type Description
returns [Permission[], { loading: boolean, error: null | Error, retry: Function }] An array containing the organization permissions and a loading status object.

@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 18, 2020

Codecov Report

Merging #52 into master will not change coverage.
The diff coverage is n/a.

Impacted file tree graph

@@           Coverage Diff           @@
##           master      #52   +/-   ##
=======================================
  Coverage   25.25%   25.25%           
=======================================
  Files          57       57           
  Lines         986      986           
  Branches      163      163           
=======================================
  Hits          249      249           
  Misses        737      737           
Flag Coverage Δ
#unittests 25.25% <ø> (ø)
Impacted Files Coverage Δ
packages/connect-core/src/entities/Organization.ts 0.00% <ø> (ø)
...ckages/connect-thegraph/src/core/GraphQLWrapper.ts 44.44% <ø> (ø)

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 95271d1...d3ba4eb. Read the comment docs.

@bpierre bpierre marked this pull request as draft June 18, 2020 12:14
@bpierre bpierre changed the title Connect for React README Connect for React API Jun 18, 2020
@bpierre bpierre marked this pull request as ready for review July 1, 2020 10:36
@bpierre bpierre requested review from 0xGabi, Evalir, andy-hook and sohkai July 1, 2020 10:36
@sohkai
Copy link
Copy Markdown
Contributor

sohkai commented Jul 1, 2020

useApps()

What is the strategy for connecting to a particular app in a react context? Will that be the domain of the app connector to define?

@bpierre
Copy link
Copy Markdown
Contributor Author

bpierre commented Jul 1, 2020

What is the strategy for connecting to a particular app in a react context? Will that be the domain of the app connector to define?

I am currently adding a .connect() method to the App object:

import { connect } from '@aragon/connect'
import VotingConnector from '@aragon/connect-thegraph-voting'

const org = await connect('myorg.aragonid.eth', 'thegraph')

let votesSubscription

org.onApps(apps => {
  const voting = apps.find(app => app.appName.startsWith('voting.'))
  const votingConnector = await voting.connect(VotingConnector)

  votesSubscription?.unsubscribe()
  votesSubscription = votingConnector.onVotes(null, votes => {
    console.log(votes)
  })
})

Then I was thinking of adding the equivalent in React:

import { useApps, useAppSubscription } from '@aragon/connect-react'
import VotingConnector from '@aragon/connect-thegraph-voting'

function App() {
  const [apps] = useApps()
  const voting = apps.find(app => app.appName.startsWith('voting.'))

  // We would take care of reusing the connection internally, for any given app + app connector.
  // Params are: app, app connector, method, method params.
  const votes = useAppSubscription(voting, VotingConnector, 'onVotes', {}) ?? []

  return (
    <ul>
      {votes.map(vote => (
        <li>{vote.id}</li>
      ))}
    </ul>
  )
}

export default () => (
  <Connect location="myorg.aragonid.eth" connector="thegraph">
    <App />
  </Connect>
)

Another option would be to reuse the app filters, to let people declare them at the Connect level:

import { useApps, useAppSubscription } from '@aragon/connect-react'
import VotingConnector from '@aragon/connect-thegraph-voting'

function App() {
  const [apps] = useApps()
  const voting = apps.find(app => app.appName.startsWith('voting.'))

  // No need to pass the connector here, it’s already set by Connect.
  const votes = useAppSubscription(voting, 'onVotes', {}) ?? []

  // It could also be a good idea to extend App with a connector property.
  // We might also want to consider having a similar system on the core library,
  // so that users could pass all their app connectors at the connect() level. 

  return (
    <ul>
      {votes.map(vote => (
        <li>{vote.id}</li>
      ))}
    </ul>
  )
}

export default () => (
  <Connect
    location="myorg.aragonid.eth"
    connector="thegraph"
    appConnectors={[

      // Everything app that matches would use the passed connector.
      // These rules would be applied one after another, so that we could
      // for example match all the voting apps with one connector, except one.
      [{ appName: 'voting.aragonpm.eth' }, VotingConnector],
      [{ appName: 'token-manager.aragonpm.eth' }, TokenConnector],

      // Like the .app() and .apps() filters
      [{ address: '0xcafe…' }, MyConnector],

      // It would also accept a function
      [app => app.appName = 'something.aragonpm.eth', MyOtherConnector],
    ]}
  >
    <App />
  </Connect>
)

Copy link
Copy Markdown
Contributor

@Evalir Evalir left a comment

Choose a reason for hiding this comment

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

Asking some questions! In general it's looking good to me. :)

Comment thread packages/connect-react/src/index.ts Outdated
Comment thread packages/connect-react/src/index.ts Outdated
Comment thread packages/connect-react/src/index.ts Outdated
Comment thread packages/connect-react/src/index.ts Outdated
Copy link
Copy Markdown
Contributor

@0xGabi 0xGabi left a comment

Choose a reason for hiding this comment

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

Solid PR, I left a few questions that we can discuss, and if they make sense include in a new PR.
Regardless LGTM! 🙌

Comment thread examples/org-viewer-react/src/Group.tsx Outdated
Comment thread packages/connect-react/src/index.ts
Comment thread packages/connect-thegraph/src/connector.ts
Comment thread packages/connect-thegraph/src/core/GraphQLWrapper.ts
Comment thread packages/connect/src/connect.ts
@andy-hook
Copy link
Copy Markdown

We spoke about this offline so just to codify my thoughts on the error and loading status handling. I think having two items in the initial return might be a bit nicer:

const [org, orgStatus] = useOrganization()

And accessing properties:

const { loading: boolean, error: null | Error, retry: Function } = orgStatus

Or perhaps:

const [state: { loading: boolean, error: null | Error}, retry: Function] = orgStatus

I don't know if you had any further thoughts @bpierre

type LoadingStatus = {
error: Error | null
loading: boolean
retry: () => void
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think this is fine for this version, but since we are dealing with subscriptions, we should probably auto-retry for the next one.

  • error would still get populated (and emptied on success).
  • It would still be possible to call retry, e.g. we could imagine a user wanting to retry immediately by clicking a button.
  • We could have a retryEvery prop on the provider, that either accept a number (milliseconds), or a function implementing a delay based on the number of attempts. Setting it to -1 would disable the automatic retry behavior.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we are dealing with subscriptions

Outside of the retryEvery prop or a user calling retry, the subscription would only help here if the underlying data was updated in the mean time, right? Or could you see a case where you hadn't connected / had some sort of corruption, and the GraphQL subscription just magically fixed it at a later point?

Copy link
Copy Markdown
Contributor Author

@bpierre bpierre Jul 4, 2020

Choose a reason for hiding this comment

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

This will really be useful e.g. for the apps, but it should be possible to wait for an organization to arrive, for example if it just got created and we are waiting for it to appear on the Subgraph. Otherwise, we could also imagine that the first attempt fails because of a connection issue, and we keep retrying until it works.

Copy link
Copy Markdown
Contributor

@0xGabi 0xGabi left a comment

Choose a reason for hiding this comment

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

LGTM!!

}

return (
<Group name="Organization">
Copy link
Copy Markdown
Contributor

@0xGabi 0xGabi Jul 5, 2020

Choose a reason for hiding this comment

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

I think this is implicit as they are used in the OrgGroups component. The one that makes me wonder is OrgPermissions because we are using loading in that one. Is this way of thinking correct?

export default function OrgPermissions() {
const [permissions, { loading }] = usePermissions()
return (
<Group name="Permissions" loading={loading}>
Copy link
Copy Markdown
Contributor

@0xGabi 0xGabi Jul 5, 2020

Choose a reason for hiding this comment

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

Related to the above comments. Do we need loading here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yes: if we are waiting for the first onPermissions() call, or if the org itself is loading.

type ConnectProps = {
children: React.ReactNode
connector: ConnectorDeclaration
location: string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I quite like location I think in the context of the Connect component should not be that vague. Otherwise how you feel about identity?

@bpierre bpierre merged commit 84d8f02 into master Jul 5, 2020
@bpierre bpierre deleted the connect-react branch July 5, 2020 22:52
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants