1. 程式人生 > 其它 >react專案使用redux入門-6-redux-thunk中介軟體處理非同步請求

react專案使用redux入門-6-redux-thunk中介軟體處理非同步請求

場景:切換語言、請求產品列表

  1. src/index.tsx

    import React from 'react'
    import ReactDOM from 'react-dom'
    import { Provider } from 'react-redux' // react-redux 利用上下文context,提供的資料元件Provider
    import 'antd/dist/antd.css'
    import './index.css'
    import App from './App'
    import 'i18n'
    
    import store from 'redux/store'
    
    ReactDOM.render(
      <React.StrictMode>
        {/* 使用Provider, 並載入 資料倉庫 store, 就可以在全域性範圍內使用store */}
        <Provider store={store}>
          <App />
        </Provider>
      </React.StrictMode>,
      document.getElementById('root')
    )
    
  2. ListPage元件中請求資料

    import { Component } from 'react'
    import { connect } from 'react-redux'
    import { ThunkDispatch } from 'redux-thunk' // ThunkDispatch<S,E,A> 為了支援函式型別的action,提供dispatch的型別 
    import { RootState } from 'redux/store'
    import { ActionsProductProps, fetchProductsActionCreator } from 'redux/product/actions'
    
    const mapStateToProps = ({ product }) => product
    // const mapStateToProps = ({ product }: RootState) => {
    //   return {
    //     loading: product.loading,
    //     productList: product.productList
    //   }
    // }
    
    const mapDispatchToProps = (dispatch: ThunkDispatch<RootState, unknown, ActionsProductProps> ) => {
      fetchProduct : () => dispatch(fetchProductsActionCreator())
    }
    
    class ListPageComponent extends Component<ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>, any> {
      
      componentDidMount() {
        const { fetchProduct } = this.props
       	fetchProduct()
      }
      
      render(){
        const { productList, loading } = this.props
        return loading? <div>正在載入……</div>:(
          <div>
            {
              productList.map(item => <p key={item.id}>{item.name}</p>)
            }    
          </div>
          )
      }
    }
    export const ListPage = connect(mapStateToProps, mapDispatchToProps)(ListPageComponent)
    
  3. Header元件中使用

    import { Component } from 'react'
    import { withRouter, RouteComponentProps } from 'react-router-dom'
    import { MenuInfo } from 'rc-menu/lib/interface'
    import { nanoid } from 'nanoid'
    // 使用react-redux
    import { Dispatch } from 'redux'
    import { connect } from 'react-redux'
    import store, { RootState } from 'redux/store'
    import { addLanguageActionCreator, changeLanguageActionCreator } from 'redux/language/actionCreators'
    
    const mapStateToProps = (state: RootState) => {
      return {
        lng: state.language.lng,
        languageList: state.language.languageList
      }
    }
    
    const mapDispatchToProps = (dispatch: Dispatch) => {
      return {
        addLanguageDispatch: (language: { code: string, language: string }) => {
          dispatch(addLanguageActionCreator(language))
        }
        changeLanguageDispatch: (lng: 'en'|'zh') => {
          dispatch(changeLanguageActionCreator(lng))
        }
      }
    }
    
    class HeaderComponent extends Component<RouteComponentProps & ReturnType<typeof mapStateToProps> & ReturnType<typeof mapDispatchToProps>>{
      render(){
        const { history, lng, languageList, addLanguageDispatch, changeLanguageDispatch } = this.props
        
        /* meun 的點選事件 */
        const oprateLanguage = ( e: MenuInfo ) => {
          if(e.key !== 'new'){
            changeLanguageDispatch(e.key)
          }else{
           addLanguageDispatch({code: `lng${nanoid()}`, language: '新語種'})
          }
        }
        
        const menu = (
          <Menu>
            <Menu.Item key='new' onClick={oprateLanguage}>
              新增新語言
            </Menu.Item>
            {languageList.map(language => (
              <Menu.Item key={language.code} onClick={oprateLanguage}>
                {language.language}
              </Menu.Item>
            ))}
          </Menu>
        )
        
        return (
          <div>
            <Typography.Text className='mr40'>讓旅遊更幸福</Typography.Text>
            <Dropdown overlay={menu}>
            	<Button>
            		{languageList.find(language => language.code === lng)?.language}
              	<GlobalOutlined />
              </Button>
     				</Dropdown>
    			</div>
        )
      }
    }
    
    export const Header = connect(mapStateToProps, mapDispatchToProps)(withRouter(HeaderComponent))
    
  4. store資料封裝

    • 新建目錄:src/reduxsrc/redux/language
    • 新建檔案:src/redux/store.tssrc/redux/language/actionCreators.tssrc/redux/language/reducers.ts
    mkdir src/redux src/redux/language
    touch src/redux/language/actionCreators.ts src/redux/language/reducer.ts
    touch src/redux/product/actions.ts src/redux/product/reducer.ts
    
    • store.ts
    import { createStore, combineReducers, applyMiddleware } from 'redux'
    import languageReducer form './language/reducer.ts'
    import productReducer form './product/reducer.ts'
    import thunk form 'redux-thunk'
    
    // 使用 combineReducers方法,連線多個reducer
    const rootReducer = combineReducers({
      language: languageReducer,
      product: productReducer
    })
    
    // redux-thunk 使用thunk中介軟體,使得redux的action除了物件形式,還是支援函式型別
    const store = createStore(rootReducer, applyMiddleware(thunk))
    
    // 使用ts的條件型別 ReturnType<T>,T:函式型別。 獲取函式返回值的型別 
    export type RootState = ReturnType<typeof store.getState>
    
    export default store
    
    • product:- product/actions.tsproduct/reducers.ts
    1. actions.ts
    // 使用redux-thunk,支援函式型別的action,這個action的型別:ThunkAction<R,S,E,A>
    import { ThunkAction } from 'react-thunk'
    import axios from 'axios'
    import { RootState } from 'redux/store'
    
    // fetch_start 主要是用來控制loading, 開始發介面,loading值為true
    export const FETCH_START = 'product/fetch_start'
    // fetch_success 主要用來 介面資料成功,將資料data存入store
    export const FETCH_SUCCESS = 'product/fetch_success'
    export const FETCH_FAIL = 'product/fetch_fail'
    
    interface ActionFetchStartProps { type: typeof FETCH_START}
    interface ActionFetchSuccessProps { type: typeof FETCH_SUCCESS, payload: any}
    interface ActionFetchFailProps { type: typeof FETCH_FAIL }
    export type ActionsProductProps = ActionFetchStartProps | ActionFetchSuccessProps | ActionFetchFailProps
    
    // actionCreator
    export const fetchStartActionCreator = (): ActionFetchStartProps => ({ type: FETCH_START})
    export const fetchSuccessActionCreator = (data:any):ActionFetchSuccessProps => ({ type: FETCH_SUCCESS, payload: data })
    export const fetchFailActionCreator = ():ActionFetchFailProps => ({ type: FETCH_FAIL })
    
    // 函式型別的action
    export const fetchProductsActionCreator = (): ThunkAction<void, RootState, unknown, ActionsProductProps>=> async (dispatch, getState) => {
      dispactch(fetchStartActionCreator())
      try{
         const { data } = await axios.get('/api/products')
         dispatch(fetchSuccessActionCreator(data))
      }catch{
        dispatch(fetchFailActionCreator())
      } 
    }
    
    
    1. reducer.ts
    import { ActionsProductProps, FETCH_START, FETCH_SUCCESS, FETCH_FAIL } from './actions.ts'
    
    interface ProductState {
      loading: boolean,
      productList: any[]
    }
    const defaultState: ProductState = {
      loading: false,
      productList: []
    }
    export default (state = defaultState, action:ActionsProductProps ) => {
        switch (action.type) {
        case FETCH_START:
          return { ...state, loading: true }
        case FETCH_SUCCESS:
          return { ...state, loading: false, tourists: action.payload }
        case FETCH_FAIL:
          return { ...state, loading: false }
        default:
          return state
      }
    }
    
    • language:- language/actions.tslanguage/reducers.ts
    1. actions.ts
    /* 用常量定義action.type,減少程式碼敲錯 */
    export const ADD_LANGUAGE = 'language/add'
    export const CHANGE_LANGUAGE = 'language/change'
    
    /* action的型別申明 */
    const AddActionProps = {
      type: typeof ADD_LANGUAGE,
      payload: { code: string, language: string }
    }
    const ChangeActionProps = {
      type: typeof CHANGE_LANGUAGE,
      payload: 'zh' | 'en'
    }
    
    export type LanguageActionProps = AddActionProps | ChangeActionProps
    
    /* 用工廠模式建立action */
    export const addLanguageActionCreator = (language: {code: string, language: string}):ADD_LANGUAGE  => {
      return {
        type: ADD_LANGUAGE,
        payload: language
      }
    }
    export const changeLanguageActionCreator = (lng: 'zh' | 'en'):CHANGE_LANGUAGE  => {
      return {
        type: CHANGE_LANGUAGE,
        payload: lng
      }
    }
    
     		2. `reducer.ts`
    
    import { ADD_LANGUAGE, CHANGE_LANGUAGE, LanguageActionProps } from './actions'
    
    export interface LanguageState {
      lng: 'zh' | 'en',
      languageList: {code: string, language: string}[]
    }
    
    const defaultStoreState: LanguageState = {
      lng: 'zh',
      languageList: [{ code: 'zh', language: '中文'}, { code: 'en', language: 'English'}]
    }
    
    
    export default (state = defaultStoreState, action:LanguageActionProps) => {
        switch (action.type) {
        case CHANGE_LANGUAGE:
          return { ...state, lng: action.payload }
        case ADD_LANGUAGE:
          return { ...state, languageList: [...state.languageList, action.payload] }
        default:
          return state
      }
    }