Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

  • Install the alpha branch of frontend build

    • npm install @edx/frontend-build@alpha

  • Add a tsconfig.json file to the top-level of your app

    • Code Block
      languagejson
      {
        "extends": "@edx/typescript-config",
        "compilerOptions": {
          "rootDir": ".",
          "outDir": "dist",
          "baseUrl": "./src",
          "paths": {
            "*": ["*"]
          }
        },
        "include": ["src/**/*"],
        "exclude": ["dist", "node_modules"]
      }
  • Update commitlint job (.github/workflows/commitlint.yml) to remove the tsconfig when running

    • See issue https://github.com/conventional-changelog/commitlint/issues/3256

    • Code Block
      languageyaml
      # Run commitlint on the commit messages in a pull request.
      
      name: Lint Commit Messages
      
      on:
        - pull_request
      
      jobs:
        commitlint:
          runs-on: ubuntu-20.04
          steps:
            - name: Check out repository
              uses: actions/checkout@v3
              with:
                fetch-depth: 0
            - name: remove tsconfig.json # see issue https://github.com/conventional-changelog/commitlint/issues/3256
              run: |
                rm -f tsconfig.json
            - name: Check commits
              uses: wagoid/commitlint-github-action@v5

...

Code Block
languagejs
// .../api-src/types.ts
export interface UserData { ...myUserDataTypeDefinition }
export interface SystemData { ...mySystemDataTypeDefinition }
export interface SubmitEventData { ...mySubmitEventDataTypeDefinition }

...

2:

...

Add query types for react-query queries.

Note: react-query does provide exported types here, but in general I found those a bit deeper/more complex than needed for this basic usage. In particular, I am relying on a snake_case upstream data source and converting it into camelCase for consumption. The TS wrapping of the useQuery calls in this pattern is not optimized for that specific interface, so much as it is for the consuming selectors.

Code Block
languagejs
// .../api-src/types.ts
...
// React-Query fields
export interface QueryStatus {
  isLoading: boolean,
  isFetching: boolean,
  isInitialLoading: boolean,
  error: unknown,
  status: string,
}

export interface QueryData<Response> extends QueryStatus {
  data: Response,
}

3: Define API hooks

...

languagejs

...

Define API hooks

Code Block
languagejs
// .../api-src/hooks/api.ts
import { useQuery, useMutation } from '@tanstack/react-query';

import { camelCaseObject } from '@edx/frontend-platform'; // or lodash
import * as types from '../types';
import { queryKeys } from '../constants';

export const useUserData = (userId: string): types.QueryData<types.UserData> => {
  const { data, ...status } =(
  useQuery({
    queryKey: [queryKeys.userData, userId],
    queryFn: () => fetch(`my-urls/user-data/${userId}`),
  });
  return { ...status, data: data ? camelCaseObject(data) : {} };
}
      .then(camelCaseObject),
  })
);

export const useSystemData = (): types.QueryData<types.SystemData> => {
  const { data, ...status } =(
  useQuery({
    queryKey: [queryKeys.systemData],
    queryFn: () => fetch('my-urls/system-data'),

 });   return { ...status, data: data ? camelCaseObject(data) : {} };
}then(camelCaseObject),
  });
);

export const useSubmit = () => useMutation({
  mutationFn: (data: types.SubmitEventData),
});

...

3: Define Selectors for the API hooks

Code Block
languagejs
// ...api-src/hooks/selectors.ts
import * as api from './api';
import * as types from './types';

// generic accessor
export const useUserDataStatus = (userId: string): types.QueryStatus => {
  const {
    isLoading,
    isFetching,
    isInitialLoading,
    status,
    error,
  } = api.useUserData(userId);
  return {
    isLoading,
    isFetching,
    isInitialLoading,
    status,
    error,
  };
};

export const useIsUserDataLoaded = (userId: string): boolean => (
  api.useUserData().status === 'success'
);
export const useUserDataResponse = (userId: string): types.UserData => (
  api.useUserData().data
);

/*
 * add more, increasingly granular UI selectors here, until all "questions"
 * asked about the data by the UI for presentation are answered in this module.
 */

...

4: Consume (Profit!)

When consuming data, ALWAYS consume with selectors. The reasoning is that this separates your UI code from the raw details of the incoming data. At all times, the consumers of the data should be asking “Questions” that can be moved around and fixed, even if the incoming data shape changes.

...

  1. Does it call useQuery with the correct queryKey array?

  2. Does it call queryFn useQuery with the expected fetch queryFn method?

  3. Does it return the status fields from they query?

  4. Does it return camel-case’d data response from the query (or {} if empty)?

...

Code Block
languagejs
// ...api-src/hooks/api.test.ts
import { useCamelCaseObject } from '@edx/frontend-platform';
import { when } from 'jest-when';

jest.mock('@tanstack/react-query', () => ({
  useQuery: jest.fn()
  useMutation: jest.fn(({ mutationFn }) => ({ mutationFn })),
}));

const mockUseQueryForUserData = (response) => {
  when(useQuery)
    .calledWith(expect.objectContaining({ queryKey: [queryKeys.userData] }))
    .mockImplementation(({ queryKey, queryFn }) => ({
      data: response,
      queryKey,
      queryFn,
    });
}
let out;
const userId = 'test-user-id';
const response = { test-field: 'user-response' };
describe('api hooks', () => {
  describe('useUserData', () => {
    beforeEach(() => {
      mockUseQueryForUserData(response);
      out = hooks.useUserData(userId);      
    });
    
    // Does it call useQuery with the correct queryKey array?
    it('initializes query with userData queryKey and user ID', () => {
      expect(out.queryKey).toEqual([queryKeys.userData, userId]);
    });
    
    // Does it call queryFn with the expected fetch method?
    it('initializes query with <my get behavior>', () => {
      // Mock/test your fetch method by calling out.queryFn()
    });;

    // Does it return the status fields from they query?
    -- Add status test from repo
    
    // Does it return thecamel-case’d statusdata fieldsresponse from theythe query (or {} if empty)?
    it('returns camelCase object from data if data has been returned', () => {
      expect(out.data).toEqual(camelCaseObject(response));
    });
    
    // Does it return
camel-case’d data response from the query (or {} if empty)?
    it('returns empty object from data if data has not been returned', () => {
      mockUseQueryForUserData(undefined);
      out = hooks.useUserData(userId);
      expect(out.data).toEqual({});
    })
  });
  // ...test system data in similar pattern.
});

...