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:
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!
}
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,
};
};
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!