Pullstate

Pullstate

  • Docs
  • GitHub

›Async Actions

Getting Started

  • Installation
  • Quick example
  • Quick example (server rendering)

Reading Store State

  • useStoreState (hook)
  • <InjectStoreState>
  • Subscribe

Updating Store State

  • update()
  • Reactions

Async Actions

  • Introduction
  • Creating an Async Action
  • Use Async Actions
  • Async Action Hooks

    • Hooks overview
    • Post action hook
    • Short circuit hook
    • Cache break hook
  • Other Async Action Options
  • Cache clearing
  • Resolve async state on the server

Dev Tools

  • Redux Devtools

Resolving async state while server-rendering

Universal fetching

Any action that is making use of useBeckon() (discussed in detail here) in the current render tree can have its state resolved on the server before rendering to the client. This allows us to generate dynamic pages on the fly!

As per our example in "Creating an Async Action"

// Create the action
const searchPicturesForTag = PullstateCore.createAsyncAction(async ({ tag }) => {
  const result = await PictureApi.searchWithTag(tag);

  if (result.success) {
    return successResult(result.pictures);
  }

  return errorResult([], `Couldn't get pictures: ${result.errorMessage}`);
});

// Use the action state in our components
export const PictureExample = (props: { tag: string }) => {
  const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });

  if (!finished) {
    return <div>Loading Pictures for tag "{props.tag}"</div>;
  }

  if (result.error) {
    return <div>{result.message}</div>;
  }

  // Inside the Gallery component we will pull our state
  // from our stores directly instead of passing it as a prop
  return <Gallery />;
};

So looking directly at beckon():

const [finished, result] = searchPicturesForTag.useBeckon({ tag: props.tag });

Using server-side async resolving, when our app hydrates for the first time on the client - finished will be true and result will already be resolved.

But very importantly: The asynchronous action code needs to be able to resolve on both the server and client - so make sure that your data-fetching functions are "isomorphic" or "universal" in nature. Examples of such functionality are the Apollo Client or Wildcard API.

In our example here, this API code would have to be "universally" resolvable:

const result = await PictureApi.searchWithTag(tag);

Excluding async state from resolving on the server

If you wish to have the regular behaviour of useBeckon() (that it instigates the async action when hit) but you don't actually want the server to resolve this asynchronous state (you're happy for it to load on the client-side only). You can pass in an option to useBeckon():

const [finished, result, updating] = GetUserAction.useBeckon({ userId }, { ssr: false });

Passing in ssr: false will cause this action to be ignored in the server asynchronous state resolve cycle.

Resolving async state on the server

Pullstate provides two ways to resolve your async state on the server - re-rendering until all state is resolved, and resolving state by running the actions directly off your initiated Pullstate "Core" before rendering.

Re-render until resolved

Until there is a better way to crawl through your react tree, the current easiest way to resolve async state on the server-side while rendering your React app is to simply render it multiple times. This allows Pullstate to register which async actions are required to resolve before we do our final render for the client.

Using the instance which we create from our PullstateCore object (see Server Rendering Example) of all our stores:

  const instance = PullstateCore.instantiate({ ssr: true });
  
  // (1)
  const app = (
    <PullstateProvider instance={instance}>
      <App />
    </PullstateProvider>
  )

  let reactHtml = ReactDOMServer.renderToString(app);

  // (2)
  while (instance.hasAsyncStateToResolve()) {
    await instance.resolveAsyncState();
    reactHtml = ReactDOMServer.renderToString(app);
  }

  // (3)
  const snapshot = instance.getPullstateSnapshot();

  const body = `
  <script>window.__PULLSTATE__ = '${JSON.stringify(snapshot)}'</script>
  ${reactHtml}`;

As marked with numbers in the code:

  1. Place your app into a variable for ease of use. After which, we do our initial rendering as usual - this will register the initial async actions which need to be resolved onto our Pullstate instance.

  2. We enter into a while() loop using instance.hasAsyncStateToResolve(), which will return true unless there is no async state in our React tree to resolve. Inside this loop we immediately resolve all async state with instance.resolveAsyncState() before rendering again. This renders our React tree until all state is deeply resolved.

  3. Once there is no more async state to resolve, we can pull out the snapshot of our Pullstate instance - and we stuff that into our HTML to be hydrated on the client.

Resolve Async Actions outside of render

If you really wish to avoid the re-rendering, Async Actions are runnable on your Pullstate instance directly as well. This will "pre-cache" these action responses and hence not require a re-render (instance.hasAsyncStateToResolve() will return false).

We make use of the following API on our Pullstate instance:

await pullstateInstance.runAsyncAction(CreatedAsyncAction, args, options);

The options parameter here is the same as that defined on the regular run() method on an action. The key options being, ignoreShortCircuit (default false) and respectCache (default false).

If you are running actions on the client side again before rendering your app for the first time (perhaps using some kind of isomorphic routing library) - you should be passing the option { respectCache: true } on the client so these actions do not run again.

This example makes use of koa and koa-router, we inject our instance onto our request's ctx.state early on in the request so we can use it along the way until finally rendering our app.

Put the Pullstate instance into the current context:

ServerReactRouter.get("/*", async (ctx, next) => {
  ctx.state.pullstateInstance = PullstateCore.instantiate({ ssr: true });
  await next();
});

Create the routes, which run the various required actions:

ServerReactRouter.get("/list-cron-jobs", async (ctx, next) => {
  await ctx.state.pullstateInstance.runAsyncAction(CronJobAsyncActions.getCronJobs, { limit: 30 });
  await next();
});

ServerReactRouter.get("/cron-job-detail/:cronJobId", async (ctx, next) => {
  const { cronJobId } = ctx.params;
  await ctx.state.pullstateInstance.runAsyncAction(CronJobAsyncActions.loadCronJob, { id: cronJobId });
  await next();
});

ServerReactRouter.get("/edit-cron-job/:cronJobId", async (ctx, next) => {
  const { cronJobId } = ctx.params;
  await ctx.state.pullstateInstance.runAsyncAction(CronJobAsyncActions.loadCronJob, { id: cronJobId });
  await next();
});

And render the app:

ServerReactRouter.get("*", async (ctx) => {
  const { pullstateInstance } = ctx.state;

  // render React app with pullstate instance
  <PullstateProvider instance={pullstateInstance}>
     <App />
  </PullstateProvider>

Even though you are resolving actions outside of the render cycle, you still need to use <PullstateProvider> as its the only way to provide your pre-run action's state to your app during rendering. If you didn't put that instance into the provider, useBeckon() can't see the result and will have queued up another run the regular way (multiple renders required).

The Async Actions you use on the server and the ones you use on the client are exactly the same - so they are really nice for server-rendered SPAs. Everything just runs and caches as needed.

You could even pre-cache a few pages on the server at once if you like (depending on how big you want the initial page payload to be), and have instant page changes on the client (and Async Actions has custom cache busting built in to invalidate async state which is too stale - such as the user taking too long to change the page).

← Cache clearingRedux Devtools →
  • Universal fetching
  • Excluding async state from resolving on the server
  • Resolving async state on the server
    • Re-render until resolved
    • Resolve Async Actions outside of render
Pullstate
Docs
InstallationA Quick ExampleMore
Community
GitHub
Pullstate
Created by Paul Myburgh