Skip to content

KSG-IT/ksg-nett-frontend

Repository files navigation

KSG-nett frontend

StackBlitz

Overview

This is the fontend repo containing source code for the webapplication KSG-nett. This is a React + Typescript application uses vite as a bundler

Support libraries

Library Function website
Apollo client Graphql queries and caching Apollo docs
Mantine Component library Mantine docs
React hot toast Toast component provider Website
React hook form Form handling Website
Yup Form validation github

In order for this app to function the backend part of the application needs to be running in the background. Follow the instructions in this repo.

Quickstart

Dependencies are managed with yarn. To run the code do the following

  1. install yarn on your computer
  2. Clone and Navigate to this folder
  3. Install the dependencies by running yarn install
  4. Run the projects by running yarn start

Branch formatting guidelines

Usually tasks are assigned through Shortcut stories, with a story type which is either a feature, bug or chore, and a story id. This is used to track progress on the task throughout development. A given branch will automatically get tracked if the branch includes sc-<story-id> anywhere in its name.

  • Our branches follows a convention of story-type/sc-<story-id>/branch-name

So a story which is a bug type, with an id of 666 and a title of "Dashboard renders wrong data" would be named bug/sc-666/dashboard-renders-wrong-data or similar.

Code-style guide

In order to keep this repository maintanable please strive to follow the following code-style guidelines.

Functional components and Typescript

In most cases when creating a new component you would want to declare it in the following way

interface NewComponentProps {...}
const NewComponent = React.FC<NewComponentProps> = ({...}) => {}

Named export

When creating a component which is imported outside its module/folder use ordinary exports (not default) and export it explicitly in the module/folder index file. If we have src/modules/ProfilePage.tsx and want to export it so we can import it for the router in src/container/AuthRoutes.tsx it is exported as follows.

// ProfilePage.tsx
export const ProfilePage = () => {...}

//index.ts
export * from './ProfilePage`

Module structure

When creating a new module, try to keep the structure as follows

src/modules
├── moduleName
│   ├── components/
│   │   ├── Component1.tsx
│   │   ├── Component2.tsx
│   │   ├── View2/
│   │   │   ├── View2Component1.tsx
│   │   │   ├── View2Component2.tsx
│   │   │   └── index.ts
│   │   └── index.ts
│   ├── views/
│   │   ├── View1.tsx
│   │   ├── View2.tsx
│   │   └── index.ts
│   ├── enums.ts
│   ├── types.ts
│   ├── types.graphql.ts
│   ├── utils.ts
│   ├── queries.ts
│   ├── mutations.ts
│   └── mutations.hooks.ts
└── moduleName2/

Where the index files uses barrel export. Components can be isolated to their own folder if the components structure in a specific view becomes fairly large and is only scoped to that module.

Apollo queries and mutations

All queries and mutations are to have their variables and returndata typed correctly where applicable. Types are to be declared in the module types.ts file where the query or mutation is defined in queries.ts or mutations.ts respectively.

Queries

Queries are defined the gql template tags in local queries.ts files. A query for a user with a specific id would look like

// queries.ts
import { gql } from ´@apollo/client`

const USER_QUERY = gql`
  query UserQuery($id: ID!) {
    User(id: $id) {
      id
      fullName
      balance
      ...
    }
  }
`

//types.graphql.ts
type UserNode = {
  id: string
  fullName: string
  ...
}


interface UserQueryVariables {
  id: string
}

interface UserQueryReturns {
  user: UserNode | null
}

Our backend schema works such that it returns null if something goes wrong. An example of this being giving a id which does not belong to any given user, so the lookup fails and the object resolves to null. Given all this a UserProfile view query would look something like this

import { useQuery } from '@apollo/client'
import { USER_QUERY } from './queries'
import { UserQueryVariables, UserQueryReturns } from './types'

const UserProfile: React.FC = () => {
  const { data, loading, error } = useQuery<
    UserQueryReturns,
    UserQueryVariables
  >(USER_QUERY,
    { variables: id: userId }
  )

  if (error) return (<SomeErrorComponent />)

  if (loading | !data) return (<SomeLoadingComponent />)

  const { user } = data

  if (user === null) return (<SomeResourceNotFoundComponent />)

  return <h1>Hello {user.fullName}. Welcome!</h1>
}

The reason we add !data to the query loading check is that data is defined by apollo as T | undefined where T is the return type we defined (this case UserQueryReturns). The Typescript compiler does not perform a deep enough check to understand that if error and loading are null and false then data has to be defined, so we do this do make the TS compiler happy.

Mutations

Mutations are effectively identical to queries in how we approach typing. The most notable thing to keep in mind is that most of the mutations available from the backend are automatically generated based of django models. Each model in most cases have a create, patch and delete mutation available. Both create and patch accept a <MutationVerb><ModelName>Input object. Given the User we would have the following types.

// mutations.ts
const PATCH_USER = gql`
  mutation PatchUser($id: ID!, $input: PatchUserInput!) {
    patchUser(id: $id, input: $input) {
      user: {
        id
        firstName
        lastName
      }
    }
  }
`

// types.ts
type CreateUserInput = {
  firstName: string
  lastName: string
}

type PatchUserInput = {
  firstname: string
  lastname: string
}

interface CreateUserMutationVariables {
  input: CreateUserInput
}

interface PatchUserMutationVariables {
  id: string
  input: PatchUserInput
}

The greatest difference between useMutation and useQeury is the returnvalue of the hook. useMutation returns an array where the first item is the function to execute the mutation and the second is different state variables similar to what useQuery returns.

// PatchUser.tsx
const PatchUser: React.FC = () => {
  const [patchUser, {data, loading} = useMutation<
    PatchUserMutationReturns,
    PatchUserMutationVariables
  >(PATCH_USER, {
    onCompleted(data){
      const { user } = data
      // Show a toast or something
    }
  }}
}

const handlePatchUser = () => {
  createUser({
    id: userId,
    input: {
      firstName: "Tormod",
      lastName: "Haugland"
    }
  })
}

return (
  <button onClick={handlePatchUser}>Create user</button>
)

This is a very simple mutation, in most cases we want do this in tandem with form handling which is covered in the next section (someday). Another thing to note is that triggering mutations also requries us to update the local state of data. This is done with the refetchQueries option in useMutation.

Mutations hooks

In order to make mutation use flexible we wrap them into hooks.

export function useUserMutations() {
  const [createUser, {loading: createUserLoading}] = useMutation<
    CreateUserMutationReturns,
    CreateUserMutationVariables
  >(CREATE_USER)

  const [patchUser, {loading: patchUserLoading}] = useMutation<
    PatchUserMutationReturns,
    PatchUserMutationVariables
  >(PATCH_USER)

  const [deleteUser, {loading: deleteUserLoading}] = useMutation<
    DeleteUserMutationReturns,
    DeleteUserMutationVariables
  >(DELETE_USER)

  return {
    createUser,
    createUserLoading,
    patchUser,
    patchUserLoading,
    deleteUser
    deleteUserLoading
  }
}

This way we can simply use them like this

const { createUser, createUserLoading } = useUserMutations()

Form handling

For most forms we try to make use of a 3-layered form design where we delegate different tasks to different layers. We have a visual form layer, a logic layer dealing with form state and validation and an API/data layer dealing with the mutation of data and fetching.

We use react-hook-form for form handling with yup as a schema validator.

A form component then has 3 files

- MyForm.tsx
- useMyFormLogic.ts
- useMyFormAPI.ts
- index.ts

index only exports the Form component giving us a simple import/export API

// MyForm.tsx
import { useMyFormLogic } from './useMyFormLogic'
import { useMyFormAPI } from './useMyFormApi'

export const MyForm: React.FC = () => {
  const { formState, formErrors, handleFormChange, handleFormSubmit } =
    useMyFormLogic(useMyFormAPI())

  return (
    <form onSubmit={handleFormSubmit}>
      <input
        type="text"
        name="firstName"
        value={formState.firstName}
        onChange={handleFormChange}
      />
      <input
        type="text"
        name="lastName"
        value={formState.lastName}
        onChange={handleFormChange}
      />
      <button type="submit">Create user</button>
    </form>
  )
}

The inspiration for this design is taken from this article.

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •  

Languages