loading...

Initial points of a React project

garryxiao profile image Garry Xiao ・5 min read

React is popular, with the understanding of its methodology, take it like Lego, all are components, build it with components. I prefer the functional components to class-based components. Writing functional components with TypeScript is a necessary combination nowadays. Except that, there are 4 points we need to determine at the elementary stage.

A. Global state management, except 'useState' for the local state of functional components and 'state' property of class-based components. Two solutions:
a) Context, designed to share data that can be considered “global” for a tree of React components. Apply it sparingly because it makes component reuse more difficult (https://reactjs.org/docs/context.html). It's better to use different Contexts for different purposes. Here I create two Contexts, UserStateContext/UserStateProvider for user state, LanguageStateContext/LanguageStateProvider for language and labels state.

In order to improve the code reuse, create a generic function to create them.
CreateState.tsx source codes:

import React from "react"
import { IState, IAction, IUpdate } from "./IState"

/**
 * Generic to create state context and provider
 * @param reducer Reduce function
 * @param initState Init state
 */
export function CreateState<S extends IState, A extends IAction>(reducer: React.Reducer<S, A>, initState: S) {
    // State context
    const context = React.createContext({} as IUpdate<S, A>)

    // State context provider
    const provider: React.FunctionComponent = (props) => {
        // Update reducer
        const [state, dispatch] = React.useReducer(reducer, initState)

        // Avoid unnecessary re-renders
        // https://alligator.io/react/usememo/
        const contextValue = React.useMemo(() => {
            return { state, dispatch }
        }, [state, dispatch])

        return (
            <context.Provider value={contextValue}>{props.children}</context.Provider>
        )
    }

    // Return
    return {
        context,
        provider
    }
}

IState.ts to define needed interfaces:

import React from "react"

/**
 * State data interface
 */
export interface IState {
}

/**
 * State action interface
 */
export interface IAction {
}

/**
 * State update interface
 */
export interface IUpdate<S extends IState, A extends IAction> {
    state: S,
    dispatch: React.Dispatch<A>
}

LanguageState.ts source code:

import { IAction } from "./IState"
import { CreateState } from "./CreateState"

/**
 * Language label
 * Indexable type
 */
export interface LanguageLabel {
    [key: string]: string
}

/**
 * Language state
 */
export interface ILanguage {
    /**
     * Global labels
     */
    labels: LanguageLabel

    /**
     * Current language name
     */
    name: string
}

/**
 * Language action to manage language and labels
 */
export interface LanguageAction extends IAction {
    /**
     * Language cid, like 'zh-CN'
     */
    name: string,

    /**
     * Labels of the language
     */
    labels: LanguageLabel
}

/**
 * Language reducer
 * @param state State
 * @param action Action
 */
export function LanguageReducer(state: ILanguage, action: LanguageAction) {
    if(action?.name) {
        return Object.assign({}, state, action)
    }

    return state
}

/**
 * Language context and provider
 */
export const { context: LanguageStateContext, provider: LanguageStateProvider } = CreateState(LanguageReducer, {} as ILanguage)

UserState.ts source codes:

import { IAction } from "./IState"
import { CreateState } from "./CreateState"

/**
 * Application user update interface
 */
export interface IUserUpdate {
    /**
     * User name
     */
    name: string
}

/**
 * Application user interface
 */
export interface IUser extends IUserUpdate {
    /**
     * Authorized or not
     */
    authorized: boolean

    /**
     * User id
     */
    id: number

    /**
     * Organization current user belongs
     */
    organization_id: number
}

/**
 * User action type
 * Style like 'const enum' will remove definition of the enum and cause module errors
 */
export enum UserActionType {
    // Login action
    Login = "LOGIN",

    // Logout action
    Logout = "LOGOUT",

    // Update action
    Update = "UPDATE"
}

/**
 * User action to manage the user
 */
export interface UserAction extends IAction {
    /**
     * Type
     */
    type: UserActionType,

    /**
     * User
     */
    user?: IUser,

    /**
     * User update
     */
    update?: IUserUpdate
}

/**
 * User reducer
 * @param state State
 * @param action Action
 */
export function UserReducer(state: IUser, action: UserAction) {
    switch(action.type) {
        case UserActionType.Login:
            return Object.assign({}, action.user)
        case UserActionType.Logout:
            return {} as IUser
        case UserActionType.Update:
            return Object.assign({}, state, action.update)
        default:
            return state
    }
}

/**
 * User context and provider
 */
export const { context: UserStateContext, provider: UserStateProvider } = CreateState(UserReducer, {} as IUser)

index.tsx source code to show how to use the Contexts:

import React from 'react'
import ReactDOM from 'react-dom'
import { UserStateProvider, LanguageStateProvider } from 'etsoo-react'
import './index.css'
import App from './App'
import * as serviceWorker from './serviceWorker'

ReactDOM.render(
  <React.StrictMode>
    <UserStateProvider>
      <LanguageStateProvider>
        <App />
      </LanguageStateProvider>
    </UserStateProvider>
  </React.StrictMode>,
  document.getElementById('root')
)

// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: https://bit.ly/CRA-PWA
serviceWorker.unregister()

b) Redux (https://redux.js.org/). By contrast with the Context solution, I would like to say, Redux is a little complex and if there is no legacy burden, just give it up.

B. Router, route-based code-splitting, ideal for Single Page Application (or SPA) (https://reacttraining.com/react-router/web/guides/quick-start).
a) Install: "npm install -g react-router-dom".
b) Install: "npm install -g @types/react-router-dom" for TypeScript support.
c) Use 'Switch', render the first child from top to bottom that matches the path.
d) Add property 'exact' to 'Route' for an exact match, especially to home with '/'.
e) Add private routes to limit page access with authentication.

Custom PrivateRoute under package 'etsoo-react' codes:

import React from 'react'
import { Redirect, Route, RouteProps } from 'react-router-dom'
import History from 'history'

/**
 * Private router property interface
 */
export interface PrivateRouteProp extends RouteProps {
    authorized: boolean
}

/**
 * Private router redirect state interface
 */
export interface PrivateRouteRedirectState {
    /**
     * referrer location
     */
    referrer: History.Location
}

/**
 * Private route for react-router-dom
 * Configue a strict route with '/login' to redirect to application's actual login page
 * In login page, useLocation() to get the Location object, and Location.state as PrivateRouteRedirectState to access the referrer
 */
export const PrivateRoute:React.FunctionComponent<PrivateRouteProp> = ({authorized, ...rest}) => {
    return (
        authorized ? <Route {...rest}/> : <Redirect to={{pathname: '/login', state: {referrer: rest.location} as PrivateRouteRedirectState}}/>
    )
}

App.tsx source codes:

import React, { useContext } from 'react'
import { Route, Switch, BrowserRouter } from 'react-router-dom'
import { PrivateRoute, UserStateContext } from 'etsoo-react'

import CustomerSearch from './customer/Search'
import CustomerView from './customer/View'
import Login from './public/Login'
import Main from './main/Main'

function App() {
  // State user
  const { state } = useContext(UserStateContext)

  // Authorized
  const authorized:boolean = (state == null ? false : state.authorized)

  return (
    <BrowserRouter>
      <Switch>
        <PrivateRoute authorized={authorized} path="/customer/search/:condition?" component={CustomerSearch} />
        <PrivateRoute authorized={authorized} path="/customer/view/:id" component={CustomerView} />
        <PrivateRoute authorized={authorized} exact path="/main" component={Main} />
        <Route component={Login} />
      </Switch>
    </BrowserRouter>
  )
}

export default App

In the login page, example codes to show how to access it:

import React from 'react'
import { useLocation } from 'react-router-dom'
import { PrivateRouteRedirectState } from 'etsoo-react'

function Login() {
    let location = useLocation()
    let state = location.state as PrivateRouteRedirectState

    return (
    <h1>Login page, referer: { state?.referrer?.pathname }</h1>
    )
}

export default Login

C. API consuming, apply 'npm install -g axios' to install Axios, or any other framework you are familiar with. It's not the key issue. As API consuming, work together with the API developer with mutual understandings of each other of standards and rules. Then in React, encapsulation is the key part. Consuming APIs in page-level components, not component parts under it, will make sure the components reuse not interrupted by API calls.

Thanks to TypeScript, make JavaScript programming much checkable and reasonable. For example, there is an API to get a list:

    /**
     * Get tiplist data
     * @param model Data model
     */
    async tiplist<M extends TiplistModel>(model: M | null = null) {
        return (await this.api.get('tiplist', { params: model })).data as IListItem[]
    }

Parameters are abstracted to a model for client calls. There is also a very similar model definition on the server-side to receive and hold the parameters.

/**
 * Tiplist model
 */
export interface TiplistModel {
    /**
     * Id
     */
    id: number,

    /**
     * Id array
     */
    ids: number[],

    /**
     * Hide id
     */
    hideId: number,

    /**
     * Hide ids array
     */
    hideIds: number[],

    /**
     * Records to read
     */
    records: number

    /**
     * Search keyword
     */
    sc: string
}

The result is also cast to an interface IListIItem array:

/**
 * Common list item interface
 */
export interface IListItem {
    // Id
    id: number

    // Label
    label: string
}

You could leave the results as pure JSON. But strong types are definitely useful even for a small project, for coding, debugging, and testing.

D. Testing
https://reactjs.org/docs/testing-recipes.html. As a team leader and project manager, For small and medium-size projects, I prefer to put the testing responsibility on coding engineers first, borrow some ideas with TDD, during the development step to speed up the output. After the project launched and somehow stable, then merge all testing codes into a unique role.

Posted on by:

garryxiao profile

Garry Xiao

@garryxiao

From China, living in NZ now, a startup founder, architect, senior software developer and team lead

Discussion

markdown guide