React18+ts4 project initialization (with routing configuration, ReduxToolkit, axios package) (with github address)

1. Initial configuration of the project

(1) Create a react + ts project

create-react-app react_ts_music –template typescript

Build directory:
image.png
Modify the configuration in running package.json
image.png
Change to:
image.png

(2) Basic project configuration

1. Craco configuration webpack

npm install @craco/craco@alpha -D

Create the craco.config.json file:

const path = require('path')


const resolve = (dir) => path. resolve(__dirname, dir)


module.exports = {<!-- -->
  webpack: {<!-- -->
    alias: {<!-- -->
      '@': resolve('src'),
    }
  }
}

2. Configure the content under the src file specified by @

Add to the tsconfig.json file:
image.png

3. Code specification configuration

Create .editorconfig file configuration
# http://editorconfig.org

root = true

[*] # means all files apply
charset = utf-8 # set the file character set to utf-8
indent_style = space # indentation style (tab | space)
indent_size = 2 # indentation size
end_of_line = lf # Control line break type (lf | cr | crlf)
trim_trailing_whitespace = true # Remove any whitespace characters at the end of the line
insert_final_newline = true # always insert a newline at the end of the file


[*.md] # means only md files apply the following rules
max_line_length = off
trim_trailing_whitespace = false
Configure prettier tool

It can make the code more beautiful and consistent in style
image.png

npm install prettier -D

Create_prettierrc file

{<!-- -->
  "useTabs": false,
  "tabWidth": 2,
  "printWidth": 80,
  "singleQuote": false,
  "trailingComma": "none",
  "semi": false
}

Change the run file
& height=202 & amp;id=u711e59d3 & amp;originHeight=303 & amp;originWidth=644 & amp;originalType=binary & amp;ratio=1.5 & amp;rotation=0 & amp;showTitle=false & amp;size= 32050 & amp;status=done & amp;style=none & amp;taskId=u281520a2-b024-48bd-bc52-3f2defc82e0 & amp;title= & amp;width=429.3333333333333″ alt=”image.png”>
image.png

# /build/*
#.local
#.output.js
# /node_modules/**


# **/*.svg
# **/*.sh


# /public/*
Configure ESlint tool

npm install eslint -D

initialization:

npx eslint –init

& height=241 & amp;id=u43c0210f & amp;originHeight=362 & amp;originWidth=1111 & amp;originalType=binary & amp;ratio=1.5 & amp;rotation=0 & amp;showTitle=false & amp;size= 74476 & amp;status=done & amp;style=none & amp;taskId=u2b16a564-03a5-4a70-a5a5-e75425c7114 & amp;title= & amp;width=740.6666666666666″ alt=”image.png”>

module.exports = {<!-- -->
    env: {<!-- -->
      browser: true,
      node: true,
      es2021:true
    },
    extends: [
      'eslint:recommended',
      'plugin:react/recommended',
      'plugin:@typescript-eslint/recommended',
    ],
    overrides: [],
    parser: '@typescript-eslint/parser',
    parserOptions: {<!-- -->
      ecmaVersion: 'latest',
      sourceType: 'module'
    },
    plugins: ['react', '@typescript-eslint'],
    rules: {<!-- -->
      '@typescript-eslint/no-var-requires': 'off',
    }
  }
  

2. Project content configuration

(1) Division of directories

image.png

(2) Configure less

npm install [email protected]

craco.config.js file:

const path = require("path");
const CracoLessPlugin = require('craco-less')
const resolve = (dir) => path. resolve(__dirname, dir);


module.exports = {<!-- -->
  plugins: [
    {<!-- -->
      plugin: CracoLessPlugin,
    }
  ],
  webpack: {<!-- -->
    alias: {<!-- -->
      "@": resolve("src"),
    },
  },
};

(3) Routing configuration

npm install react-router-dom

Create index.tsx in the router folder

import React,{lazy} from "react";
import {Navigate} from 'react-router-dom'
import {RouteObject} from 'react-router-dom'
const Discover = lazy(() => import('@/views/discover'))


const routes: RouteObject[]=[
  {
    path: '/',
    element: <Navigate to="/discover" />
  },
  {
    path: '/discover',
    element: <Discover />,
    children: [

    ]
  },
]


export default routes
import React,{Suspense} from "react";
import {useRoutes,Link} from 'react-router-dom'
import routes from './router'
function App() {
  return <div className="App">
    <div className="topHeader">
      <Link to="/discover">discover</Link>
      <Link to="/users">users</Link>
      <Link to="/task">task</Link>
    </div>
    <Suspense fallback="Loading...">
      <div className="main">
        {useRoutes(routes)}
      </div>
    </Suspense>
  </div>;
}

export default App;

import React from "react";
import ReactDOM from "react-dom/client";
import {BrowserRouter} from 'react-router-dom'
import App from "@/App";
import "@/assets/css/base.less"
const root = ReactDOM.createRoot(
  document.getElementById("root") as HTMLElement
);
root.render(<BrowserRouter><App /></BrowserRouter>);

(4) redux configuration

Install react-redux: react-redux and @reduxjs/toolkit.

npm install react-redux @reduxjs/toolkit -S

Start writing basic RTK programs after installing the relevant packages

  • Create a store folder
  • Create an index.ts as the main entry
  • Create a festivals folder to hold all stores
  • Create a counterSlice.js file and export simple addition and subtraction methods

Creating a Redux State Slice

Creating a slice requires a string name to identify the slice, an initial state, and one or more reducer functions that define how the state should be updated. After the slice is created, we can export the Redux action creators and reducer functions generated in the slice.
image.png
store/features/counterSlice.js

import { createSlice } from '@reduxjs/toolkit'

// Create a Slice
export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0,
  },
  reducers: {
    // define an add method
    increment:(state,{payload}) => {
      state.value += parseInt(payload)
    },
    // define a subtraction method
    decrement: state => {
      state.value -= 1
    },
  },
})

// Export addition and subtraction methods
export const { increment, decrement } = counterSlice. actions

// expose reducers
export default counterSlice.reducer

createSlice is a fully automatic method for creating reducer slices. Its internal calls are createAction and createReducer. This is the reason why those two are introduced first. createSlice requires an object as a parameter, and the configuration information of the reducer is specified in the object through different attributes.
createSlice(configuration object)
Properties in the configuration object:

  • name – the name of the reducer, which will be used as the prefix of the type attribute in the action, do not repeat
  • initialState – the initial value of the state
  • reducers — the specific method of the reducer, which requires an object as a parameter, and the reducer can be added in the form of a method, and RTK will automatically generate an action object.

In general, after using createSlice to create a slice, the slice will automatically generate an action and a reducer based on the configuration object. The action needs to be exported to the calling site, and the calling site can use the action as a dispatch parameter to trigger the state modification. The reducer needs to be passed to configureStore for it to take effect in the repository.
We can see what counterSlice and counterSlice.actions look like
image.png

Add Slice Reducers to Store

Next, we need to import the reducer function from the count slice and add it to our store. By defining a field in the reducer parameter, we tell the store to use this slice reducer function for all updates to the state.
We used to use redux directly like this

const reducer = combineReducers({
  counter:counterReducers
});

const store = createStore(reducer);

store/index.js
The reducer attribute of the slice is the reducer automatically created by the slice according to the method we passed. It needs to be passed as a reducer into the configuration object of configureStore to make it effective:

import { configureStore } from '@reduxjs/toolkit'
import { useSelector,useDispatch } from 'react-redux'
import type { TypedUseSelectorHook } from 'react-redux'
import counterSlice from './features/counterSlice'
 
// configureStore creates a redux data
const store = configureStore({
  // Merge multiple Slices
  reducer: {
    counter: counterSlice,
  },
})

//app's hook, ts type definition
export type IRootState=ReturnType<typeof store.getState>
type AppDispatch = typeof store.dispatch
export const useAppSelector: TypedUseSelectorHook<IRootState> = useSelector
export const useAppDispatch: () => AppDispatch = useDispatch
export default store
  • configureStore needs an object as a parameter, in which different attributes can be used to set the store, for example: reducer attribute is used to set the reducer associated with the store, preloadedState is used to specify the initial value of the state, etc., and some other values We will explain it later.
  • The reducer attribute can pass a reducer directly, or an object as a value. If only one reducer is passed, it means there is only one reducer in the store. If you pass an object as a parameter, each property of the object can execute a reducer, and it will automatically merge these reducers inside the method.

store added to the global

main.js

import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'

// redux toolkit
import { Provider } from 'react-redux'
import store from './store'

ReactDOM.createRoot(document.getElementById('root')).render(
  <Provider store={store}>
    <App />
  </Provider>,
)

Using Redux state and actions in React components

Now we can use React-Redux hooks to make React components interact with the Redux store. We can use useSelector to read data from the store and use useDispatch to dispatch actions.
App.jsx

import React, { memo ,useRef,createRef} from 'react'
import { increment, decrement } from '@/store/features/counterSlice'
import type { FC, ReactNode } from 'react'
// import { IRootState } from '@/store'
import { useAppSelector ,useAppDispatch} from '@/store'
interface IProps {
  children?: ReactNode
}

const Artist: FC<IProps> = () => {
  const count = useAppSelector((state) => state. counter. value)
  const dispatch = useAppDispatch()
  const InputRef = useRef<HTMLInputElement>(null);
  const InputRef2 = createRef();
  return (
    <div>
      <h1>count</h1>
      <div style={<!-- -->{ width: 100, margin: '10px' }}>
        <input ref = { InputRef } type="text"/>
        <button onClick={() => dispatch(increment(InputRef.current?.value))}> + </button>
        <button onClick={() => dispatch(decrement())}>-</button>

        <span>{count}</span>
      </div>
    </div>
  )
}

export default memo(Artist)

image.png
Now, whenever you click the “increment” and “decrement” buttons.

  • Will dispatch the corresponding Redux action to the store
  • The reducer corresponding to the counter slice will see the action and update its state
  • The component will see the new state from the store and re-render the component with the new data

(5) axios configuration

Under the service file: Create the requset folder:

service/request:

Path to configure interface:

const BASE_URL = 'http://codercba.com:9002/'
const TIME_OUT = 10000

console.log(process.env.REACT_APP_NAME)
console.log(process.env.REACT_APP_AGE)

export { BASE_URL, TIME_OUT }

Interceptor type:

import type { AxiosRequestConfig, AxiosResponse } from 'axios'

export interface HYRequestInterceptors<T = AxiosResponse> {
  requestInterceptor?: (config: AxiosRequestConfig) => AxiosRequestConfig
  requestInterceptorCatch?: (error: any) => any
  responseInterceptor?: (res: T) => T
  responseInterceptorCatch?: (error: any) => any
}

export interface HYRequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
  interceptors?: HYRequestInterceptors<T>
  showLoading?: boolean
}

Encapsulation of the request class:

import axios from 'axios'
import type {<!-- --> AxiosInstance } from 'axios'
import type {<!-- --> HYRequestInterceptors, HYRequestConfig } from './type'


const DEAFULT_LOADING = true


class HYRequest {<!-- -->
  instance: AxiosInstance
  interceptors?: HYRequestInterceptors
  showLoading: boolean


  constructor(config: HYRequestConfig) {<!-- -->
    // create axios instance
    this.instance = axios.create(config)


    // save basic information
    this.showLoading = config.showLoading DEAFULT_LOADING
    this.interceptors = config.interceptors


    // use interceptor
    // 1. The interceptor taken from config is the interceptor of the corresponding instance
    this.instance.interceptors.request.use(
      // this.interceptors?.requestInterceptor,
      this.interceptors?.requestInterceptorCatch
    )
    this.instance.interceptors.response.use(
      this.interceptors?.responseInterceptor,
      this.interceptors?.responseInterceptorCatch
    )


    // 2. Add interceptors that all instances have
    this.instance.interceptors.request.use(
      (config) => {<!-- -->
        return config
      },
      (err) => {<!-- -->
        return err
      }
    )


    this.instance.interceptors.response.use(
      (res) => {<!-- -->
        const data = res.data
        if (data.returnCode === '-1001') {<!-- -->
          console.log('request failed~, error message')
        } else {<!-- -->
          return data
        }
      },
      (err) => {<!-- -->
        // Example: Judging different HttpErrorCode to display different error messages
        if (err. response. status === 404) {<!-- -->
          console.log('404 error~')
        }
        return err
      }
    )
  }


  request<T = any>(config: HYRequestConfig<T>): Promise<T> {<!-- -->
    return new Promise((resolve, reject) => {<!-- -->
      // 1. Single request processing of request config
      if (config.interceptors?.requestInterceptor) {<!-- -->
        config = config.interceptors.requestInterceptor(config)
      }


      // 2. Determine whether loading needs to be displayed
      if (config. showLoading === false) {<!-- -->
        this.showLoading = config.showLoading
      }


      this. instance
        .request<any, T>(config)
        .then((res) => {<!-- -->
          // 1. Processing of data by a single request
          if (config.interceptors?.responseInterceptor) {<!-- -->
            res = config.interceptors.responseInterceptor(res)
          }
          // 2. Set showLoading to true, so that it will not affect the next request
          this.showLoading = DEAFULT_LOADING


          // 3. Return the result resolve
          resolve(res)
        })
        .catch((err) => {<!-- -->
          // Set showLoading to true so that it will not affect the next request
          this.showLoading = DEAFULT_LOADING
          reject(err)
          return err
        })
    })
  }


  get<T = any>(config: HYRequestConfig<T>): Promise<T> {<!-- -->
    return this.request<T>({<!-- --> ...config, method: 'GET' })
  }


  post<T = any>(config: HYRequestConfig<T>): Promise<T> {<!-- -->
    return this.request<T>({<!-- --> ...config, method: 'POST' })
  }


  delete<T = any>(config: HYRequestConfig<T>): Promise<T> {<!-- -->
    return this.request<T>({<!-- --> ...config, method: 'DELETE' })
  }


  patch<T = any>(config: HYRequestConfig<T>): Promise<T> {<!-- -->
    return this.request<T>({<!-- --> ...config, method: 'PATCH' })
  }
}


export default HYRequest

service/modules encapsulates the interface to be used:

import hyRequest from '..'


export function getTopBanner() {<!-- -->
  return hyRequest.get({<!-- -->
    url: '/banner'
  })
}


export function getHotRecommend() {<!-- -->
  return hyRequest.get({<!-- -->
    url: '/personalized'
  })
}


export function getNewAlbum(offset = 0, limit = 10) {<!-- -->
  return hyRequest.get({<!-- -->
    url: '/album/new',
    params: {<!-- -->
      offset,
      limit
    }
  })
}


export function getPlayListDetail(id: number) {<!-- -->
  return hyRequest.get({<!-- -->
    url: '/playlist/detail',
    params: {<!-- -->
      id
    }
  })
}

service/index

Encapsulation of request functions

// service unified export
import HYRequest from './request'
import {<!-- --> BASE_URL, TIME_OUT } from './request/config'


const hyRequest = new HYRequest({<!-- -->
  baseURL: BASE_URL,
  timeout: TIME_OUT,
  interceptors: {<!-- -->
    requestInterceptor: (config) => {<!-- -->
      return config
    },
    requestInterceptorCatch: (err) => {<!-- -->
      return err
    },
    responseInterceptor: (res) => {<!-- -->
      return res
    },
    responseInterceptorCatch: (err) => {<!-- -->
      return err
    }
  }
})


export default hyRequest

Routed request:

import hyRequest from '@/service';
import React, { memo,useEffect,useState} from 'react'
import type { FC, ReactNode } from 'react'
import {getTopBanner} from '@/service/modules/recommend'
interface IProps {
  children?: ReactNode
}
interface listRoot {
  imageUrl: string
  targetId: number
  adid: any
  targetType: number
  titleColor: string
  typeTitle: string
  url: any
  exclusive: boolean
  monitor Impress: any
  monitorClick: any
  monitorType: any
  monitorImpressList: any
  monitorClickList: any
  monitorBlackList: any
  extMonitor: any
  extMonitorInfo: any
  adSource: any
  adLocation: any
  adDispatchJson: any
  encodeId: string
  program: any
  event: any
  video: any
  song: any
  scm: string
  bannerBizType: string
}

const AxiosTemplate: FC<IProps> = () => {
  const [list, setList]=useState<listRoot[]>([])
  useEffect(() =>{
    (async function() {
      const res = await getTopBanner()
      setList(res. banners)
    })()
  },[])
  return <div>
    <h1>I am the data requested by axios:</h1>
    <ul>
      {
        list. map((item) => {
          return (
            <li key={item.encodeId}>{item.imageUrl}</li>
          )
        })
      }
    </ul>
  </div>
}

export default memo(AxiosTemplate)

3.github code address:

https://github.com/wzz778/react_ts_template