Tips for running workflows in background (FrontEnd-ReactJS)

Hey folks, hope you’re doing well!

We’re using Temporal with NestJS in our BackEnd and it’s working perfect! Right now we are using Redis to save the changes of these activities in the cache. From the frontend side we’re using useQuery in order to make http requests to the BackEnd and return the latest result persisted in the cache.

Please let me know if there is a better way to communicate the results of these activities with the frontend.
BTW, Is there any way to resume workflows with service workers in order to run these tasks in background from any page? If so, do you have any suggestions with ReactJS/RemixJS?

Thanks for your support!

If I understood correctly, you’re saving activity results in Redis in order to read it in the front-end.

If that’s the case, it’s fine if you need direct access to results with low latency. However, it might become cumbersome to add data to Redis after every activity.

What I’m doing is leveraging queries to running workflows if I need data that is inherently part of a workflow’s state. I’m also using React Query, althought with tRPC, for this.

1 Like

Hey Bruno, hope you’re doing well!

running workflows if I need data that is inherently part of a workflow’s state

Do you have any example of those queries? Sounds interesting!! <3

Thanks for your response and happy Friday!

Best,
Juan

Well, I’ve created a simple store with React Context in order to run workflows with React and React Query as well, this is the reducer of my store:

import { produce } from 'immer';

import { StoreState, StoreActions, StoreAction } from './types';

const canUseDOM = !!(
  typeof window !== 'undefined' &&
  window.document &&
  window.document.createElement
);

function getInitialState(): StoreState {
  const savedState = canUseDOM ? window.localStorage.getItem('store') : null;

  return savedState ? JSON.parse(savedState) : {
    workflows: {},
  };
}

export const initialState = getInitialState();

export const updateLocalStorage = (state: StoreState) => canUseDOM && window.localStorage.setItem('store', JSON.stringify(state));

export const reducer = (state = initialState, action: StoreAction): StoreState => produce(state, draftState => {
  switch (action.type) {
    case StoreActions.RunWorkflow:
      draftState.workflows = draftState.workflows || {};
      draftState.workflows[action.workflowType] = draftState.workflows[action.workflowType] || [];
      draftState.workflows[action.workflowType].push(action.payload);
      updateLocalStorage(draftState);
      break;
    case StoreActions.ClearWorkflow:
      draftState.workflows = draftState.workflows;
      const workflowsToClear = draftState.workflows[action.workflowType] || [];
      const clearIndex = workflowsToClear.findIndex(({ referenceId }) => action.referenceId === referenceId);
      if (clearIndex > -1) {
        workflowsToClear.splice(clearIndex, 1);
        updateLocalStorage(draftState);
      }
      break;
    case StoreActions.UpdateWorkflow:
      draftState.workflows = draftState.workflows || {};
      const workflowsToUpdate = draftState.workflows[action.workflowType] || [];
      const updateIndex = workflowsToUpdate.findIndex(({ referenceId }) => action.payload.referenceId === referenceId);
      if (updateIndex > -1) {
        workflowsToUpdate[updateIndex] = action.payload;
        updateLocalStorage(draftState);
      }
      break;
    default:
      break;
  }
});

Then I was able to create a single-use hooks to load workflows from the store and resume those queries also if the user reloads the page or navigate to other pages in the website, E.g:

  • utils:
export type WorkflowType = 'list' | 'mint'; // all workflow types!

export interface Workflow<T> {
  referenceId: string;
  step: string | 'cancel';
  data: T;
  error?: Error;
  isCanceling?: boolean;
}

export interface ListWorkflow extends Workflow<object> {
  step: 'start' | 'status' | 'cancel';
  // additional properties for this workflow here!
}

  • useWorkflowActions:
import { StoreActions, useStore } from '~/store';
import { Workflow, WorkflowType } from './utils';
import {
  HandleClearEvent,
  HandleErrorEvent,
  HandleRunEvent,
  HandleUpdateEvent,
} from './types';

type WorkflowActionsProps = {
  workflowType: WorkflowType;
}

export const useWorkflowActions = <T extends Workflow<unknown>>({ workflowType }: WorkflowActionsProps) => {
  const [store, dispatch] = useStore();

  const handleError: HandleErrorEvent<T> = ({
    workflow,
    error,
  }) => {
    dispatch({
      type: StoreActions.UpdateWorkflow,
      workflowType,
      payload: {
        ...workflow,
        error,
      },
    });
  };

  const handleRun: HandleRunEvent<T> = ({ workflow }) => {
    dispatch({
      type: StoreActions.RunWorkflow,
      workflowType,
      payload: workflow,
    });
  };

  const handleUpdate: HandleUpdateEvent<T> = ({ workflow }) => {
    dispatch({
      type: StoreActions.UpdateWorkflow,
      workflowType,
      payload: workflow,
    });
  };

  const handleClear: HandleClearEvent<T> = ({ workflow }) => {
    dispatch({
      type: StoreActions.ClearWorkflow,
      referenceId: workflow.referenceId,
      workflowType,
    });
  };

  const handleCancel: HandleUpdateEvent<T> = ({ workflow }) => {
    dispatch({
      type: StoreActions.UpdateWorkflow,
      workflowType,
      payload: {
        ...workflow,
        step: 'cancel',
        isCanceling: true,
      },
    });
  };

  return {
    handleRun,
    handleError,
    handleClear,
    handleUpdate,
    handleCancel,
  };
};

  • useListWorkflows:
import {
  ListWorkflow,
  WorkflowType,
  cancelListWorkflow,
} from './utils';
import { useQueries } from '@tanstack/react-query';

import { useWorkflowActions } from './useWorkflowActions';

export type WorkflowProps = {
  workflows: Array<ListWorkflow>;
};

export type ListStatus =
  | 'Pending'
  | 'Submitted'
  | 'Failed'
  | 'Failed_Already_Listed'
  | 'Cancelled';
const WORKFLOW_TYPE: WorkflowType = 'list';
const SUCCESS_STATUS: Array<ListStatus> = ['Submitted', 'Failed_Already_Listed'];
const FAILURE_STATUS: Array<ListStatus> = ['Failed', 'Cancelled'];
const END_STATUS = [...SUCCESS_STATUS, ...FAILURE_STATUS];

export const useListWorkflow = ({ workflows }: WorkflowProps) => {
  const { handleError, handleClear, handleUpdate } =
    useWorkflowActions<ListWorkflow>({ workflowType: WORKFLOW_TYPE });

  // Using parallel queries to run all list workflows at the same time!
  return useQueries({
    queries: workflows
      .filter((w) => !w.error)
      .map((workflow) => {
        return {
          cacheTime: 0,
          queryKey: ['list-workflow', workflow.step, workflow.referenceId],
          queryFn: async () => {
            switch (workflow.step) {
              case 'status':
                // check status of the workflow, we can load the data from a cache (Redis, etc)
              case 'cancel':
                await cancelListWorkflow(workflow.referenceId);
              case 'start':
              default:
                // Create a referenceId for the workflow and start that workflow from your BackEnd
            }
          },
          enabled: true,
          retry: true,
          onSuccess: (response: unknown) => {
            // Choose the next step of the workflow
            switch (workflow.step) {
              case 'status':
                // use types for casting your BackEnd response, e.g:
                const listStatus = <StatusResponse>response;
                if (SUCCESS_STATUS.includes(listStatus?.status)) {
                  console.log('List workflow Success', response);
                  handleClear({ workflow });
                } else if (FAILURE_STATUS.includes(listStatus?.status)) {
                  // handleError(...)
                }
                break;
              case 'cancel':
                handleUpdate({ workflow: { ...workflow, step: 'status' } });
                break;
              case 'start':
              default:
                handleUpdate({
                    workflow: {
                      ...workflow,
                      step: 'status',
                      referenceId: response,
                    },
                  });
                break;
            }
          },
        };
      }),
  });
};

In this way we’re able to support new workflows in the future by creating single-use hooks like this example that can handle all the steps of each workflow!

However, it might become cumbersome to add data to Redis after every activity.

It’s working great now, but maybe loading the data from the workflow state is another great alternative, at least we’re loading all these data from endpoints of our BackEnd for FrontEnd, so changing that logic is really simple! <3

Thanks for your help mate!

2 Likes

Hey folks, I forgot to share about the store file, this is the example:

  • store.tsx:
import React, {
  useReducer,
  createContext,
  PropsWithChildren,
  ComponentType,
} from 'react';

import { initialState, reducer } from './reducer';
import { StoreReducer, ContextProps } from './types';

export const GlobalContext = createContext<ContextProps>([
  initialState,
  () => null,
]);
export const StoreConsumer = GlobalContext.Consumer;

export type StoreProviderProps = PropsWithChildren;

export const StoreProvider: React.FC<StoreProviderProps> = ({
  children,
}) => {
  const [state, dispatch] = useReducer<StoreReducer>(reducer, {
    ...initialState,
  });
  const value = React.useMemo<ContextProps>(
    () => [state, dispatch],
    [state, dispatch],
  );

  return (
    <GlobalContext.Provider value={value}>{children}</GlobalContext.Provider>
  );
};

export type StoreProviderType = {
  accessToken?: string;
};

export function withStoreProvider<T>(
  WrappedComponent: ComponentType<T>,
): ComponentType<T & StoreProviderType> {
  return function (props: T & StoreProviderType) {
    return (
      <StoreProvider>
        <WrappedComponent {...props} />
      </StoreProvider>
    );
  };
}

export function useStore() {
  return React.useContext(GlobalContext);
}

export function useStoreState() {
  return React.useContext(GlobalContext)[0];
}

export function useStoreDispatch() {
  return React.useContext(GlobalContext)[1];
}
  • types:
 import { Reducer, Dispatch } from 'react';
import { Workflow, WorkflowType } from './utils';

export enum StoreActions {
  RunWorkflow,
  ClearWorkflow,
  UpdateWorkflow,
}

export type StoreState = {
  workflows: Record<WorkflowType, Array<Workflow<unknown>>>;
};

export type StoreAction =
  | {
      type: StoreActions.RunWorkflow;
      payload: Workflow<unknown>;
      workflowType: WorkflowType;
    }
  | {
      type: StoreActions.ClearWorkflow;
      referenceId: Workflow<unknown>['referenceId'];
      workflowType: WorkflowType;
    }
  | {
      type: StoreActions.UpdateWorkflow;
      payload: Workflow<unknown>;
      workflowType: WorkflowType;
    };

export type StoreReducer = Reducer<StoreState, StoreAction>;
export type StoreDispatch = Dispatch<StoreAction>;

export type ContextProps = [StoreState, StoreDispatch];

Hope this can help someone working with this as well! <3

I created this project/template about this integration with ReactJS: GitHub - proyecto26/projectx: Tame full-stack chaos with Temporal workflows and React wizardry, the ultimate event-driven architecture for your apps 🧙‍♂️✨