impact-context
v0.1.4
Published
Reactive contexts for React
Downloads
3
Maintainers
Readme
impact-context
Install
yarn add impact-context
Description
You probably already know contexts from React. Contexts is the primitive you use to share and manage state scoped to a component tree. Impact contexts are also just React contexts, but they have a reactive implementation. That means you use reactive state primitives instead of the state primitives tied to the reconciliation loop of React. You are freed from the performance challenges and the mental overhead of sharing state and management of state through React contexts.
Learn
Creating a context
A context is just a function that returns state and/or management of state, just like any other traditional React context. We call it a context for familiarity, but you will learn that Impact contexts has benefits over traditional React contexts.
import { context } from 'impact-context'
function SomeContext() {
return {}
}
const useSomeContext = context(SomeContext)
Providing and consuming contexts
The context
function returns a hook that can be used in components and other Impact contexts. The hook has a property called Provider
which is a context provider component that exposes the context to React.
import { context } from 'impact-context'
function SomeContext() {
return {}
}
const useSomeContext = context(SomeContext)
function SomeComponent() {
const someContext = useSomeContext()
}
function App() {
return (
<useSomeContext.Provider>
<SomeComponent />
</useSomeContext.Provider>
)
}
Consuming contexts in other contexts
A great thing about React contexts is that you can consume other contexts higher up in the component tree. Impact contexts allows the same. The hook returned from a context can be used in both components and other Impact contexts.
import { context } from 'impact-context'
import { useGlobalContext } from '../useGlobalContext'
function SomePageContext() {
const { api } = useGlobalContext()
return {}
}
return useSomePageContext = context(SomePageContext)
Disposing
When a context provider is unmounted it will be disposed. The cleanup
function registers a callback that will be called when this disposal occurs. Since Impact contexts runs outside the reconciliation loop this function is guaranteed to run only when the provider is unmounted.
import { context, cleanup } from 'impact-context'
import { useGlobalContext } from '../useGlobalContext'
function SomePageContext() {
const { api } = useGlobalContext()
const disposeSubscription = api.subscribeSomething(() => {
// Update a signal or whatever
})
cleanup(disposeSubscription)
return {}
}
export const useSomePageContext = context(SomePageContext)
Passing props to contexts
When your context is exposed through its Provider
any props are passed to the context.
import { context } from 'impact-context'
function SomePageContext({ id }: { id: string }) {
return {}
}
const useSomePageContext = context(SomePageContext)
const App = ({ id }: { id: string }) => {
return (
<useSomePageContext.Provider id={id}>
<SomePage />
</useSomePageContext.Provider>
)
}
Lazy loading contexts
Just like traditional context providers you simply lazy load the component that has the provider of the context.
import { lazy } from 'react'
// ProjectPage is providing a context
const ProjectPage = lazy(() => import('./ProjectPage'))
const Layout = () => {
return (
<ProjectPage />
)
}
Organising contexts
It can be a good idea to structure your application the way your components are nested. Instead of of creating directories of flat components, hooks etc. you rather start with the root component and create a nested structure. Now your file structure reflects you UI composition, but also your file tree also shows what components can consume what contexts.
/pages
/PageA
index.tsx
usePageAContext.ts
/PageB
/Sidebar
index.tsx
useSidebarContext.ts
index.tsx
usePageBContext.ts
index.tsx
useGlobalContext.ts
The index.tsx
entry points is responsible for providing the related context to the component tree. What is great about this is that your pages/features does not only get a context, but you can use the same entry to provide any initial data for the context, suspense and an error boundary to handle errors within that page/feature.
import { Suspense, use } from 'react'
import { Errorboundary } from 'react-error-boundary'
import { useGlobalContext } from '../useGlobalContext'
import { useProjectContext } from './useProjectContext'
import { Layout } from './components/Layout'
import { ProjectOverview } from './components/ProjectOverview'
import { ConfigureProject } from './components/ConfigureProject'
export function Project({ id }: { id: string }) {
const { api } = useGlobalContext()
const projectData = use(api.projects.fetch(id))
return (
<ErrorBoundary>
<Suspense fallback={<h4>Loading...</h4>}>
<useProjectContext.Provider key={id} data={projectData}>
<Layout>
<ProjectOverview />
<ConfigureProject />
</Layout>
</useProjectContext.Provider>
</Suspense>
</ErrorBoundary>
)
}
This pattern gives you a lot of insight about the application just looking at the files and folders in your project:
- It tells you how your components are composed
- It tells you what contexts are available to what component trees
- It tells your where data fetching boundaries are
- It tells you where your error boundaries are
- It tells you where your suspense boundaries are
Concurrent mode
With concurrent mode React fully embraces the fact that components needs to be pure. That means you can not use useRef
or useState
to instantiate something with side effects, as you can not reliably dispose of them. The reason is that the concurrent mode could run the component body several times without running useEffect
to clean things up.
For Impact to work the ReactiveContextProvider
creates a ContextContainer
which needs to be disposed on unmount. This is exactly what is not possible to achieve with concurrent mode. The great thing though is that a ContextContainer
by itself is not a side effect, there is nothing in there, just references to what "can be there". It is only when the provider is mounted and children components starts consuming the context that it is "instantiated".
That does not solve disposal completely though, cause a useEffect
might also run multiple times. That is why the ReactiveContextProvider
uses a component class with componentDidUmount
to trigger disposal. This lifecycle method only runs when the component actually unmounts.
But that actually does not completely solve the challenge. React might call componentDidUnmount
, but still keep reference to the component and mount it again. This happens for example during suspense. Impact solves this by making the ReactiveContextProvider
and create the ContextContainer
during its render. If there is no existing ContextContainer
, or it has been disposed, it will create a new one. This changes the context value and guarantees consuming components will resolve it again. The final event on this component is a componentDidUnmount
which will guarantee disposing of the context.