Skip to content

Feature request: support multiple services with one provider #14

@adamdickinson

Description

@adamdickinson

As mentioned in the README, having many services offered by an app gets a bit intense. Because of how context works, it means we'd be looking at a lot of top-level nesting.

There is another possible option however - shared context.

Implementation Goal

The best APIs are designed for those who use them, not those who build them, so it's worth looking at how we might like to apply a single-provider service structure:

import { Services } from '@adamdickinson/react-service'

import { AuthService } from './services/auth'
import { NoticeService } from './services/notice'
import { ReportService } from './services/report'

const App = () => (
  <Services services={[AuthService, NoticeService, ReportService]}>
    ...
  </Services>
)
import { useAuth } from './services/auth'

const SubComponent = () => {
  const { user } = useAuth()
  return <h1>{user?.name ?? 'Not logged in'}</h1>
}

And if we wanted to, we should be able to expose services at various points in our app:

const App = () => (
  <>
    <Services auth={AuthService}>
      <Services report={ReportService}>
        ...
      </Services>
    </Services>

    <Services notice={NoticeService}>
      ...
    </Services>
  </>
)

The smarter way to handle this would be as follows, but the above illustrates being able to expose many services with only a few providers well:

const App = () => (
  <>
    <AuthService>
      <ReportService>
        ...
      </ReportService>
    </AuthService>

    <NoticeService>
      ...
    </NoticeService>
  </>
)

How do we accomplish this?

A single context seems to be the way...

const ServiceContext = React.createContext()

interface ServicesProps {
  services: Service[]
}

const Services = ({ children, ...services }) => {
  const existingApis = useContext(ServiceContext)
  const apis = { 

    // Carry forward higher-level services
    ...existingApis

    // Add newly defined services
    ...Object.keys(services).reduce((newApis, name) => {
      apis[name] = services[name].useApi()
    }, newApis)

  }

  return (
    <ServiceContext.Provider value={apis}>
      {children}
    </ServiceContext.Provider>
  )
}

Benefits

The simple benefit is that we can now throw in a heap of services without a nesting nightmare, but we could do that without many architectural changes, so why does the above proposal attempt to wrap everything in a single context? Because it allows us to get services talking to each other. Let's say you have a Data service that uses Auth info, and an Auth service that will make use of unauthorised processes in the Data service - this structure will allow us to do this.

More thoughts on the way on this one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions