import { inject, injectable, provide, useDependencies } from '@servicetitan/react-ioc'; import { observer } from 'mobx-react'; import { QueryApiStore } from '../utils/query-api.store'; import type { QueryApiOptions } from '../utils/query.api'; import { Fragment, useState } from 'react'; import { QueryClientStore } from '../utils/query-client.store'; import { makeAutoObservable } from 'mobx'; interface Job { id: number; title: string; description: string; } @injectable() class JobsNavigationStore { jobId: number | null = null; staleTime = 1000 * 60 * 10; // 10 minutes hasInitialData = true; constructor() { makeAutoObservable(this); } navigateToJob = (id: number) => { this.jobId = id; }; navigateToJobs = () => { this.jobId = null; }; setStaleTime = (staleTime: number) => { this.staleTime = staleTime; }; setHasInitialData = (hasInitialData: boolean) => { this.hasInitialData = hasInitialData; }; } @injectable() class JobsApi { async get(): Promise<{ data: Job[] }> { // eslint-disable-next-line no-console console.log('Fetching jobs...'); // Simulate an API call return new Promise(resolve => { setTimeout(() => { resolve({ data: [ { id: 1, title: 'Job 1', description: 'Description 1' }, { id: 2, title: 'Job 2', description: 'Description 2' }, ], }); }, 2000); }); } async getJob(id: number): Promise<{ data: Job }> { // eslint-disable-next-line no-console console.log(`Fetching job with id ${id}...`); // Simulate an API call return new Promise(resolve => { setTimeout(() => { resolve({ data: { id, title: `Job ${id}`, description: `Description ${id}` }, }); }, 2000); }); } } @injectable() class JobsStore extends QueryApiStore { @inject(JobsApi) protected api?: JobsApi; @inject(JobsNavigationStore) protected navigationStore?: JobsNavigationStore; get queryOptions(): QueryApiOptions { return { queryKey: ['demo', 'jobs'], queryFn: async () => (await this.api?.get())?.data || [], staleTime: this.navigationStore?.staleTime, }; } } @injectable() class JobStore extends QueryApiStore { @inject(JobsApi) protected api?: JobsApi; @inject(JobsNavigationStore) protected navigationStore?: JobsNavigationStore; get queryOptions(): QueryApiOptions { const jobId = this.navigationStore?.jobId; return { queryKey: ['demo', 'jobs', jobId], queryFn: async () => (await this.api?.getJob(jobId!))?.data, enabled: Boolean(jobId), initialData: (() => { if (!this.navigationStore?.hasInitialData) { return undefined; } const jobs = this.queryClientStore?.queryClient.getQueryState([ 'jobs', ])?.data; return jobs?.find(j => j.id === jobId); })(), staleTime: this.navigationStore?.staleTime, }; } } const Jobs = provide({ singletons: [JobsApi, JobsStore], })( observer(() => { const [{ navigateToJob }, { data, isFetching, isLoading }] = useDependencies( JobsNavigationStore, JobsStore ); return (

Jobs

isFetching: {isFetching ? 'true' : 'false'}

isLoading: {isLoading ? 'true' : 'false'}

    {data?.map(job => (
  • {job.description}

  • ))}
); }) ); const Job = provide({ singletons: [JobsApi, JobStore], })( observer(() => { const [{ navigateToJobs }, { data, isFetching, isLoading }] = useDependencies( JobsNavigationStore, JobStore ); return (

{data?.title}

isFetching: {isFetching ? 'true' : 'false'}

isLoading: {isLoading ? 'true' : 'false'}

Description: {data?.description}

); }) ); export const RelatedQueriesExample = provide({ singletons: [JobsNavigationStore, QueryClientStore], })( observer(() => { const [showApp, setShowApp] = useState(false); const [{ hasInitialData, jobId, setHasInitialData, setStaleTime, staleTime }] = useDependencies(JobsNavigationStore); return (

Example with related queries

In this example, a Jobs component is first rendered. The Jobs component uses a store with query key ['demo', 'jobs'] and a query to fetch jobs.

If a job is clicked, the Job component is rendered. The Job component uses a store with query key ['demo', 'jobs', id] and a query to fetch a single job.


If you set initialData to "cached job", then the JobStore's{' '} {`queryOptions: { initialData }`} will be set to the cached data for the corresponding job. This means that when a Job is rendered, no loading occurs because the cached data is already available. staleTime is respected.

If it is set to "undefined", then no initialData will be set, and you will notice a load occurring on the first render of that Job.

{' '}

staleTime defaults to 10 minutes, and controls how long before data is considered stale and needs to be refetched in the background. This uses a technique called "stale-while-revalidate" to ensure that cached data is available to display while it is being updated in the background.

When set to 10 minutes, when you navigate back and forth between Jobs and a single Job, you will notice that isFetching is false, which means no fetch is occurring. If you set staleTime to{' '} 0, data will always be considered stale immediately and you will notice isFetching will be true as fetching occurs when navigating.

staleTime:{' '} setStaleTime(Number(e.target.value))} />

{!showApp && ( )} {showApp && (jobId ? : )}
); }) );