Skip to content
module@warp-drive/react

This package provides a React-specific reactivity integration, components and hooks for WarpDrive. The Guides at the end of this page cover installing and using it.

The Basics ​

  1. Provide your app with a store.
src/app.tsx
tsx
import { StoreProvider } from '@warp-drive/react';
import { Store } from './store.ts';
import { UserList } from './user-list.tsx';

export function App(props) {
  return (
    <div className='App'>
      <StoreProvider Store={Store}>
        <h1>Hello React!</h1>
        <UserList />
      </StoreProvider>
    </div>
  );
}
  1. Make your First Request.
src/users.tsx
tsx
import { Request } from '@warp-drive/react';
import { listUsers } from './api.ts';

export function UserList() {
  return (
    <Request
      query={listUsers()}
      states={{
        loading: ({ state }) => <div>Loading Users...</div>,
        error: ({ error, features }) => (
          <div>
            <p>Error: {error.message}</p>
            <p><button onClick={features.retry}>Try Again?</button></p>
          </div>
        ),
        content: ({ result, features }) => (
          <ul>
            {result.data.map(user => <li>{user.name}</li>)}
          </ul>
        ),
      }}
    />
  );
}

How It Works ​

Unlike reactive frameworks, React does not natively have the ability to support fine-grained reactivity. However, we can approximate it to "good enough" granularity by keeping track of signals used within a specific ReactiveContext.

React also does not have a built-in way to memoize arbitrary functions based on what signals they consume the way that reactive frameworks do, but by building overtop of other Signal libraries we can provide this.

Due to the above limitations, the React Signals integration is built overtop @warp-drive/alien-signals, a signals graph built on alien-signals.

Its Watcher especially is valuable here, as it allows us to subscribe to changes to the dependency graph of a memo and not just a signal.

Every <ReactiveContext /> provides a Watcher and subscribes to it as an External Store. When reactive state is accessed, if there is a current context then the signal is added to that context's Watcher.

The Watcher then intelligently batches updates to the signals it is observing and then notifies React that the external store has changed. The triggers React to rerender the components in the context's sub-tree.

Optimizing Rerenders ​

The <Request /> component also functions as a <ReactiveContext/> (you do not need to wrap the component in one yourself). For most applications, relying only on the <Request /> component may be good enough. But when performance counts you might find that wrapping additional sub-trees in their own <ReactiveContext/> is better.

For instance, imagine a request that loads a list of users. We might wrap each individual user in its own <ReactiveContext/>.

tsx
function UserPreview({ user }) {
  return (
    <ReactiveContext>
      <h3>{user.name} | {user.jobTitle}</h3>
    </ReactiveContext>
  );
}

function UserList() {
  return (
    <Request
      query={getFirstPageOfUsers()}
      states={{
        loading: ({ state }) => <div>Loading user data...</div>,
        error: ({ error, features }) => (
          <div>
            <p>Error: {error.message}</p>
            <p><button onClick={features.retry}>Try Again?</button></p>
          </div>
        ),
        content: ({ result, features }) => (
          <ul>
          {result.data.map(user => <li><UserPreview user={user} /></li>)}
          </ul>
        </div>
      ),
      }}
    />
  );
}

Using React Embedded In Another Reactive Framework ​

When React components share a page and a store with another framework, such as an Ember app that renders some React components, import @warp-drive/alien-signals/install before either framework's install import:

ts
import '@warp-drive/alien-signals/install';
import '@warp-drive/ember/install';
import '@warp-drive/react/install';

The other framework then registers its signals with the same graph React watches, and memoized values come from that graph, so components in both frameworks re-render when the data they read changes. See Using Ember and React on the Same Page for the full setup.

Derivations only track WarpDrive's signals

With @warp-drive/alien-signals/install, memoized values such as derived fields only recompute when a signal WarpDrive manages changes. If a derivation reads state that only a framework tracks, such as an Ember @tracked property or a React useState value, it keeps returning its cached value when that state changes. Keep the state a derivation reads in WarpDrive, for instance as a field on the resource it derives from.

Tips & Tricks aka "The Rule of WarpDrive" ​

  1. only things accessed during a render are subscribed to

Values accessed asynchronously inside of effects or callbacks will not become dependencies unless the reactive property is explicitly a dependency of the effect or callback. Which brings us to rule #2.

  1. useEffect/useMemo/useCallback etc will only re-run if the reactive property is one of their dependencies

This bit is easy enough. If you want the effect or callback to re-run anytime a rerender is due to reactive state having changed, you can consume the context value itself as a dependency.

ts
import { WatcherContext } from '@warp-drive/react';

function OnAnyReactiveUpdate() {
  useEffect(() => console.log('updated'), [WatcherContext]);
}
  1. if signals are passed from an external source, their consumption by the app needs to occur within render.

This means that top-level consumption by the app must be done within a component, see below.

ts
// the reactive state is created external to the React application
const blogPost = await store.request(getBlogPost());

// We wrapped the usage in a ReactiveContext, and yet 🤔... strangely
// it does not reactively update
const root = createRoot(element);
root.render(
   <ReactiveContext><div>{blogPost.content.title}</div></ReactiveContext>
)
ts
// the reactive state is created external to the React application
const blogPost = await store.request(getBlogPost());

// wrap the consumption in a component
function MyApp() {
  return (
    <div>{blogPost.content.title}</div>
  }
}

// render the react app wrapped in a <ReactiveContext />
const root = createRoot(element);
root.render(
  <ReactiveContext><MyApp /></ReactiveContext>
)

The reason this is so is because when react compiles the jsx in the BAD example, it will treat blogPost.content.title as an external dependency of the app, and thus the access of it will only occur once and cannot be subscribed to by React's render.

Here is a babel repl to see what this looks like in compiled output.

By moving the access inside of MyApp, our reactive state is now a dependency of the component's render, and will work as expected.

Guides ​

  • Installation: install @warp-drive/react and add the @warp-drive/react/install import to your app.
  • Configuration: configure the build plugin and create a Store.
  • Setup - React: provide the store to your components with <StoreProvider /> and read it with useStore.
  • Reactivity: how WarpDrive uses signals to notify your UI.
  • Making Requests: make a request with <Request />, or with getRequestState inside a <ReactiveContext />.
  • Reactive Control Flow: render a request's loading, error and content states.

Components ​

Contexts ​

Hooks ​

Other ​

Released under the MIT License.