代码之家  ›  专栏  ›  技术社区  ›  kojow7

React/Redux中的通用还原剂/作用

  •  7
  • kojow7  · 技术社区  · 6 年前

    我正在尝试确定如何在同一个组件中使用多个数据段。

    Cards . 所以,如果用户点击 /cards/hockey /cards/football ,它应该遵循相同的过程,检查是否存储了数据,如果没有从API中提取数据,并显示包含该数据的Cards页面。

    stats 关于不同运动队的数据。

    我不会总是提前知道什么类型的卡是可用的,所以我不能在我的应用程序中硬编码特定的运动类型。

    所以在本例中,我只想创建两个组件:cards和stats,但需要动态加载数据来填充这些组件。

    现在我有太多的重复进行,它是硬编码。这意味着,如果不创建新代码来处理这些类型中的每一个,我就不能在将来动态地添加新类型。

    举个例子,现在我有了行动/足球卡片.js和/或行动/曲棍球卡片行动.js. 然后我有一个减速机/足球减速机.js和/或减速器/曲棍球减速机.js. 对于Stats组件,我可能也有类似的组件。

    我还指定了状态,例如 FETCH_HOCKEY_CARDS_SUCCESS FETCH_FOOTBALL_CARDS_SUCCESS .

    我试图遵循的一个例子是 https://scotch.io/tutorials/bookshop-with-react-redux-ii-async-requests-with-thunks

    我能做些什么使我的代码更通用,这样我就不需要硬编码特定的数据集了。有没有什么好的教程可以处理类似的情况?

    进一步澄清

    我的一个组件(屏幕)是运动卡屏幕。菜单系统(带链接)是从API在站点加载时自动生成的,所以我并不总是知道有哪些链接可用。所以,可能有曲棍球,足球,以及其他一些我没有想到的运动的链接。单击菜单链接时,它将调用该运动类型的API,并在运动卡屏幕上显示数据。

    基于上面的链接(以及其他类似的站点),我已经在actions and reducers部分找到了如何硬编码每个特定运动的请求,但是如果我不提前了解运动,我还无法找到一般的方法。

    根据目前的答复进一步澄清

    如果有人在API数据库中添加了一个名为MuffiBall的新sport,我的应用程序就需要能够处理它。因此,我不能期望为每个添加到API的新运动添加新的JavaScript代码。

    我当前代码的概要

    索引.js

    //index.js
    //Other imports here (not shown)
    import Cards from './components/CardsPage'
    import * as cardActions from './actions/cardActions';
    import * as statsActions from './actions/statsActions';
    
    import configureStore from './store/configureStore';
    
    const store = configureStore();
    
    /* Bad place to put these, and currently I am expected to know what every sport is*/
    store.dispatch(hockeyActions.fetchHockey());
    store.dispatch(footballActions.fetchFootball());
    store.dispatch(muffiballActions.fetchMuffiball());
    
    
    render(
      <Provider store={store}>
              <Router>
                    <div>
    
                        /* Navigation menu here (not shown) */
                        /* Currently it is manually coded, */
                        /* but I will be automatically generating it based on API */
    
                          <Route exact path="/" component={Home} />
                          <Route path="/about" component={About} />
                          <Route path="/cards/:val" component={Cards} />
                          <Route path="/stats/:val" component={Stats} />
                    </div>
              </Router>
      </Provider>,
      document.getElementById('app')
    );
    

    商店/配置存储.js

    // store/configureStore.js
    import {createStore, compose, applyMiddleware} from 'redux';
    // Import thunk middleware
    import thunk from 'redux-thunk';
    import rootReducer from '../reducers';
    
    export default function configureStore(initialState) {
      return createStore(rootReducer, initialState,
        // Apply to store
        applyMiddleware(thunk)
      );
    }
    

    // actions/actionTypes
    
    export const FETCH_HOCKEY_SUCCESS = 'FETCH_HOCKEY_SUCCESS';
    export const FETCH_FOOTBALL_SUCCESS = 'FETCH_FOOTBALL_SUCCESS';
    export const FETCH_MUFFIBALL_SUCCESS = 'FETCH_MUFFIBALL_SUCCESS';
    

    行动/曲棍球动作.js(每项运动一个这样的文件-需要制作一个通用文件):

    // hockeyActions.js (one such file for every sport - need to make this one generic file):
    
    import Axios from 'axios';
    
    const apiUrl = '/api/hockey/';
    // Sync Action
    export const fetchHockeySuccess = (hockey) => {
      return {
        type: 'FETCH_HOCKEY_SUCCESS',
        hockey
      }
    };
    
    
    //Async Action
    export const fetchHockey = () => {
      // Returns a dispatcher function
      // that dispatches an action at a later time
      return (dispatch) => {
        // Returns a promise
        return Axios.get(apiUrl)
          .then(response => {
            // Dispatch another action
            // to consume data
    
            dispatch(fetchHockeySuccess(response.data))
          })
          .catch(error => {
            console.log(error)
            throw(error);
          });
      };
    };
    

    异径管/曲棍球减速机.js(每项运动有一个这样的文件-需要制作一个通用文件)

    // reducers/hockeyReducers.js (one such file for every sport - need to make this one generic file)
    
    import * as actionTypes from '../actions/actionTypes'
    
    export const hockeyReducer = (state = [], action) => {
      switch (action.type) {
        case actionTypes.FETCH_HOCKEY_SUCCESS:
              return action.hockey;
        default:
              return state;
      }
    };
    

    // reducers/index.js
    
    import { combineReducers } from 'redux';
    import {hockeyReducer} from './hockeyReducers'
    import {footballReducer} from './footballReducers'
    import {muffiballReducer} from './muffiballReducers'
    
    export default combineReducers({
      hockey: hockeyReducer,
      football: footballReducer,
      muffiball: muffiballReducer,
      // More reducers for each sport here
    });
    

    组件/卡片页.js:

    //components/CardsPage.js
    
    import React from 'react';
    import { connect } from 'react-redux';
    
    class Cards extends React.Component{
      constructor(props){
        super(props);
    
        this.state = {
            data: this.props.data,
        }
    
      }
    
      componentWillReceiveProps(nextProps){
            this.setState({
                    data: nextProps.data,
            })
      }
    
      render(){
    
        return(
            {/* cards displayed from this.state.data */}
        )
      }
    }
    
    const mapStateToProps = (state, ownProps) => {
      return {
        data: state[ownProps.match.params.val]
      }
    };
    
    export default connect(mapStateToProps)(Cards);
    
    4 回复  |  直到 6 年前
        1
  •  5
  •   lecstor    6 年前

    退一步,找出具有独特形状的数据类型,例如 cards stats

    export const fetchCards = (sport) => {
      return (dispatch) => {
        return Axios.get(`/api/${sport}/`)
          .then(response =>
            dispatch(fetchCardSuccess({ sport, data: response.data }))
          )
          .catch(error => {
            console.log(error)
            throw(error);
          });
      };
    };
    

    减速机

    export const cardReducer = (state = {}, action) => {
      switch (action.type) {
        case actionTypes.FETCH_CARD_SUCCESS:
          return { ...state, [action.sport]: action.data };
        default:
          return state;
      }
    };
    

    卡片选择器

    export const getSport(state, sport) {
      return state.cards[sport];
    }
    

        2
  •  4
  •   pizzarob    6 年前

    你可以买个普通的 <Results /> 组件。不确定如何进行路由,但可以使用URL的路径名来确定要获取和显示的数据。

    路由组件(React Router 4)可以如下所示:

    <Route path="/cards/:id" render={props => <Results {...props} />}
    

    然后在你的 <Results/> 可以使用的组件 react-redux componentDidMount 你可以看看你有没有合适的数据。如果您没有合适的数据,那么从componentDidMount发送一个操作来获取它。像这样的

    import { connect } from 'react-redux';
    import React from 'react';
    import { fetchDataAction } from './actions';
    
    class Results extends React.Component {
      componentDidMount() {
        // check if results exists, if not then fire off an action to get 
        // data. Use whatever async redux pattern you want
        if (!this.props.results) {
          this.props.fetchData();
        }
      }
    
      render() { /* DO SOMETHING WITH RESULTS, OR LACK OF */ }
    }
    
    const mapStateToProps = (state, ownProps) => ({
      results: state.results[ownProps.match.params.id],
    });
    
    const mapDispatchToProps = (dispatch, ownProps) => ({
      fetchData() {
        // send path parameter via action to kick off async fetch
        dispatch(fetchDataAction(ownProps.match.params.id));
      },
    });
    
    export default connect(mapStateToProps, mapDispatchToProps)(Results);
    

    您可以有一个结果缩减器,它只是一个将类别映射到结果的对象。结果如下:

    export default (state = {}, action) => {
      switch(action.type) {
        case 'FETCH_LOADED':
          const { payload: { type, results } } = action;
          return {
            ...state,
            [type]: results,
          };
        default:
          return state;
      };
    };
    
        3
  •  3
  •   Cody S    6 年前

    Redux Ducks . 这里有一个 good helper library and example 在你的代码库中实现这个。

    以上面链接中的示例为基础,您可以看到:

    // remoteObjDuck.js
    
    import Duck from 'extensible-duck'
    import axios from 'axios'
    
    export default function createDuck({ namespace, store, path, initialState={} }) {
      return new Duck({
        namespace, store,
    
        consts: { statuses: [ 'NEW', 'LOADING', 'READY', 'SAVING', 'SAVED' ] },
    
        types: [
          'UPDATE',
          'FETCH', 'FETCH_PENDING',  'FETCH_FULFILLED',
          'POST',  'POST_PENDING',   'POST_FULFILLED',
        ],
    
        reducer: (state, action, { types, statuses, initialState }) => {
          switch(action.type) {
            case types.UPDATE:
              return { ...state, obj: { ...state.obj, ...action.payload } }
            case types.FETCH_PENDING:
              return { ...state, status: statuses.LOADING }
            case types.FETCH_FULFILLED:
              return { ...state, obj: action.payload.data, status: statuses.READY }
            case types.POST_PENDING:
            case types.PATCH_PENDING:
              return { ...state, status: statuses.SAVING }
            case types.POST_FULFILLED:
            case types.PATCH_FULFILLED:
              return { ...state, status: statuses.SAVED }
            default:
              return state
          }
        },
    
        creators: ({ types }) => ({
          update: (fields) => ({ type: types.UPDATE, payload: fields }),
          get:        (id) => ({ type: types.FETCH, payload: axios.get(`${path}/${id}`),
          post:         () => ({ type: types.POST, payload: axios.post(path, obj) }),
          patch:        () => ({ type: types.PATCH, payload: axios.patch(`${path}/${id}`, obj) })
        }),
    
        initialState: ({ statuses }) => ({ obj: initialState || {}, status: statuses.NEW, entities: [] })
      })
    }
    

    每一项运动都会创造一只鸭子,重复使用相同的功能。

    曲棍球:

    // hockeyDuck.js
    
    import createDuck from './remoteObjDuck'
    
    export default createDuck({ namespace: 'my-app', store: 'hockeyCards', path: '/cards/hockey' })
    

    // footballDuck.js
    
        import createDuck from './remoteObjDuck'
    
        export default createDuck({ namespace: 'my-app', store: 'footballCards', path: '/cards/football' })
    

    // 异径管.js

    import { combineReducers } from 'redux'
    import footballDuck from './footballDuck'
    import hockeyDuck from './hockeyDuck'
    
    export default combineReducers({ [footballDuck.store]: footballDuck.reducer, [hockeyDuck.store]: hockeyDuck.reducer })
    

    如果您想动态地向redux添加reducer,您必须使用以下方法: https://github.com/ioof-holdings/redux-dynamic-reducer . 然后可以根据API调用响应动态创建duck:

    //get from API
    var sport = "football";
    var footballDuck = createDuck({ namespace: 'my-app', store: 'cards', path: `/cards/${sport}` });
    store.attachReducer({ [footballDuck.store]: footballDuck.reducer });
    
        4
  •  -2
  •   Estefano Timoteo    6 年前
    // structure (something like...)
    
    /*
    ./components 
    ./redux
    ./redux/actions
    ./redux/reducers
    ./redux/sagas
    ./redux/types
    ./util
    */
    
    /* ------------------------------------------------- */
    
    /* package.json */
    
    {
      (...)
      "proxy": "http://localhost:3000",
      (...)  
    }
    
    /* ------------------------------------------------- */
    
    /* index.js or otherComponent.js */
    
    import React from 'react' 
    import { render } from 'react-dom'
    import { createStore, applyMiddleware } from 'redux'
    import { Provider } from 'react-redux'
    import reducers from './redux/reducers/index'
    import logger from 'redux-logger'
    import createSagaMiddleware from 'redux-saga'
    import indexSagas from './redux/sagas/indexSagas'
    
    import { environment } from './util/baseUrl'
    
    const sagaMiddleware = createSagaMiddleware()
    
    const store = 
      environment === 'DEV' ?
        createStore(
          reducers,
          window.__REDUX_DEVTOOLS_EXTENSION__ && 
          window.__REDUX_DEVTOOLS_EXTENSION__(),  
          applyMiddleware(sagaMiddleware, logger)
        ) :
        createStore(
          reducers,
          applyMiddleware(sagaMiddleware)
        ) 
    
    sagaMiddleware.run(indexSagas)
    
    render(
       <Provider store={store}>
          <App />      
       </Provider>,
     document.getElementById('app'))
    
    
    /* ------------------------------------------------- */
    
    /* baseURL.js */
    
    const DEV = 'DEV'
    const PROD = 'PROD'
    
    /*-----------------------------------------*/
    /*------*/ export const environment = DEV /* <------- */
    /*-----------------------------------------*/
    
    export const baseURL = 
        environment === DEV ?
        '/api/v1/' : 
        'https://abcde.website.net/api/v1/' 
    
    /* ------------------------------------------------- */
    
    /* genericTypes.js */
    
    export const GET_REGISTERS_REQUEST = 'GET_REGISTERS_REQUEST'
    export const GET_REGISTERS_SUCCESS = 'GET_REGISTERS_SUCCESS'
    export const GENERIC_ERROR_MSG = 'GENERIC_ERROR_MSG'
    
    /* ------------------------------------------------- */
    
    /* actions.js */
    
    export const getRegistersRequest = ( route ) => {
      return {
        type: GET_REGISTERS_REQUEST,
        route,
      }
    }
    export const getRegistersSuccess = ( data ) => {
      return {
        type: GET_REGISTERS_SUCCESS,
        data,
      }
    } 
    export const genericErrorMsg = ( errorMsg ) => {
      return {
        type: GENERIC_ERROR_MSG,
        errorMsg,
      }
    }
    
    /* ------------------------------------------------- */
    
    /* genericReducer.js */
    
    import { GET_REGISTERS_REQUEST, GET_REGISTERS_SUCCESS, GENERIC_ERROR_MSG } from '../types/genericTypes'
    
    const INITIAL_STATE = {
      data: [],
      isFetching: false,
      isLoaded: false,
      error: false,
      errorMsg: '',
    }
    
    const genericReducer = (state = INITIAL_STATE, action) => {
      switch(action.type){
        case GET_REGISTERS_REQUEST:
          return {
            ...state,
            data: [],
            isFetching: true,
            isLoaded: false,
            error: false,
            errorMsg: '',
          }  
        case GET_REGISTERS_SUCCESS:
          return {
            ...state,
            data: action.data,
            isFetching: false,
            isLoaded: true,
          }
        case GENERIC_ERROR_MSG: 
          return {
            ...state,
            isFetching: false,
            error: true,
            errorMsg: action.errorMsg,
          }   
        default:
          return state
      }
    }
    export default genericReducer  
    
    /* ------------------------------------------------- */
    
    /* yourComponent.js  */
    
    import React, { Component } from "react"
    import { connect } from 'react-redux'
    import { getRegistersRequest } from '../../redux/actions'   
    
    //(...)
    // this.props.getRegistersRequest('cards/hockey')
    // this.props.getRegistersRequest('cards/football')
    //(...)
    
    const mapStateToProps = (state) => {
      return {
        data: state.genericReducer.data,
        isFetching: state.genericReducer.isFetching,
        isLoaded: state.genericReducer.isLoaded,
        error: state.genericReducer.error,
        errorMsg: state.genericReducer.errorMsg,
      } 
    }
    const mapDispatchToProps = (dispatch) => {
      return {
        getRegistersRequest: ( route ) => dispatch(getRegistersRequest( route )),
      }
    }
    export default connect(mapStateToProps, mapDispatchToProps)(yourComponent)
    
    /* ------------------------------------------------- */
    
    /* indexSagas.js */
    
    import { takeLatest } from 'redux-saga/effects'
    import axios from 'axios'
    
    import { GET_REGISTERS_REQUEST } from '../types/genericTypes'
    import { getRegistersRequest } from './genericSagas'
    
    function* indexSagas() {
      try {
        yield (takeLatest(GET_REGISTERS_REQUEST, getRegistersRequest, axios))  
      }
      catch (e) {
        // (...)
      }
    }
    export default indexSagas  
    
    /* ------------------------------------------------- */
    
    /* genericSagas.js */
    
    import { put } from 'redux-saga/effects'
    
    import { getRegistersSuccess, genericErrorMsg } from '../actions'
    
    export function* getRegistrosRequest(axios, action) {
      const rest = createRest(axios)
      try {
        let route = ''
        switch (action.route) {
          case 'cards/hockey':
          case 'cards/football':
            route = action.route
            break
          default: {
            yield put(genericErrorMsg('Route [ ' + action.route + ' ] not implemented yet!'))
            return
          }
        }    
        const data = yield rest.get(route)
        yield put(getRegistersSuccess(data))
      }
      catch (e) {
        yield put(genericErrorMsg(e))
      }
    }
    
    /* ------------------------------------------------- */
    
    /* createRest */
    
    import { baseURL } from '../../util/baseUrl'
    function createRest(axios){
      const token = localStorage.getItem('yourToken')
      const rest = axios.create({
        baseURL: baseURL,
        headers:{
          Authorization: 'Bearer ' + token
        }
      })
      return rest  
    }
    export default createRest
    
    /* ------------------------------------------------- */
    

    希望有帮助!