简体   繁体   中英

How can I refactor this ASYNC call in my react component to make it more readable?

I want my component to fetch an array of objects from the server. Each object is a message with author, body and date. I then want to render these messages in my react component.

My react component currently fetches data from the server before mounting. It will then store this message list in the redux state.|

I'm sure there's a better way of writing this code. 1. Can I place the fetch request in either the Action or Reducer file? 2. Can I write a function in the component to make the async call?

import React, { Component } from 'react';
import Message from '../components/message.jsx';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
// Actions
import { fetchMessages } from '../actions/actions_index.js';

class MessageList extends Component {
  constructor(props) {
    super(props)
  }

  componentWillMount() {
    fetch('https://wagon-chat.herokuapp.com/general/messages')
        .then(response => response.json(),
          error => console.log('An error occured receiving messages', error))
        .then((data) => {
          this.props.fetchMessages(data.messages);
        });
  }

  render() {
    return (
      <div className="message-list">
        {this.props.messageList.map( (message, index) => { return <Message key={index} message={message}/> })}
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    messageList: state.messageList
  }
}

function mapDispatchToProps(dispatch) {
  return bindActionCreators(
    { fetchMessages: fetchMessages },
    dispatch
  )
}

export default connect(mapStateToProps, mapDispatchToProps)(MessageList);

  1. Can I place the fetch request in either the Action or Reducer file?

The fetch request should be placed in action creator. Where the retrieved data will be dispatched to reducer later to manipulate the data, and lastly update the store to show on UI. Here's simple flow for most of react-redux app.

UI -> Action creator (calling request, saga etc..) -> reducer -> store -> UI

  1. Can I write a function in the component to make the async call?

Yes, this should be called action creator , and you can see actions.js below for more reference.

I think you can safely follow this sample pattern where most tutorials out there apply. I'm assuming all files listed here are in the same directory.

constant.js

const MESSAGE_FETCH__SUCCESS = 'MESSAGE/FETCH__SUCCESS'
const MESSAGE_FETCH__ERROR = 'MESSAGE/FETCH__ERROR'
export {
  MESSAGE_FETCH__SUCCESS,
  MESSAGE_FETCH__ERROR
}

actions.js

import {
  MESSAGE_FETCH__SUCCESS,
  MESSAGE_FETCH__ERROR
} from './constant';

const fetchMessageError = () => ({
  type: MESSAGE_FETCH__ERROR
})

const fetchMessageSuccess = data => ({
  type: MESSAGE_FETCH__SUCCESS,
  payload: data
})

const fetchMessages = () => {
  const data = fetch(...);

  // if error 
  if (data.error)
    fetchMessageError();
  else fetchMessageSuccess(data.data);
}

export {
  fetchMessages
}

reducers.js

import {
  MESSAGE_FETCH__SUCCESS,
  MESSAGE_FETCH__ERROR
} from './constant';

const INIT_STATE = {
  messageList: []
}

export default function( state = INIT_STATE, action ) {
  switch(action.type) {
    case MESSAGE_FETCH__SUCCESS:
      return {
        ...state,
        messageList: action.payload
      }
    case MESSAGE_FETCH__ERROR:
      // Do whatever you want here for an error case
      return {
        ...state
      }
    default:
      return state;
  }
}

index.js

Please read the comment I noted

import React, { Component } from 'react';
import Message from '../components/message.jsx';
import { connect } from 'react-redux';

// Actions
import { fetchMessages } from './actions';

class MessageList extends Component {
  /* If you don't do anything in the constructor, it's okay to remove calling `constructor(props)`
  */
  //constructor(props) {
  //    super(props)
  //}

  // I usually put this async call in `componentDidMount` method
  componentWillMount() {
    this.props.fetchMessage();
  }

  render() {
    return (
      <div className="message-list">
        {
          /* Each message should have an unique id so they can be used 
          for `key` index. Do not use `index` as an value to `key`. 
See this useful link for more reference: https://stackoverflow.com/questions/28329382/understanding-unique-keys-for-array-children-in-react-js
          */
          this.props.messageList.map( message => <Message key={message.id} message={message}/> )
        }
      </div>
    )
  }
}

function mapStateToProps(state) {
  return {
    messageList: state.messageList
  }
}

export default connect(mapStateToProps, {
  fetchMessages
})(MessageList);

You could use redux-thunk in an action called getMessages.

So: (The double arrow func, is to return an action, see redux-thunk)

const getMessages = ()=>(dispatch, getState)=>{
    fetch('https://wagon-chat.herokuapp.com/general/messages')
    .then(response => response.json(),
      error => dispatch(['error', error]))
    .then((data) => {
      dispatch(data);
    })
}

Then you've successfully reduced your component to:

componentWillMount(){
    this.props.getMessages()
}

I think @Duc_Hong answered the question.

And in my opinion, I suggest using the side-effect middle-ware to make AJAX call more structured, so that we could handle more complicated scenarios (eg cancel the ajax request, multiple request in the same time) and make it more testable.

Here's the code snippet using Redux Saga

// Actions.js

const FOO_FETCH_START = 'FOO\FETCH_START'
function action(type, payload={}) {
  return {type, payload};
}
export const startFetch = () => action{FOO_FETCH_START, payload);

// reducer.js

export const foo = (state = {status: 'loading'}, action) => {
  switch (action.type) {
  case FOO_FETCH_STARTED: {
    return _.assign({}, state, {status: 'start fetching', foo: null});
  }
  case FOO_FETCH_SUCCESS: {
    return _.assign({}, state, {status: 'success', foo: action.data});
  }
  ......
  }
};
  1. Can I place the fetch request in either the Action or Reducer file?

// Saga.js, I put the ajax call (fetch, axios whatever you want) here .

export function* fetchFoo() {
  const response = yield call(fetch, url);
  yield put({type: FOO_FETCH_SUCCESS, reponse.data});
}

// This function will be used in `rootSaga()`, it's a listener for the action FOO_FETCH_START
export function* fooSagas() {
  yield takeEvery(FOO_FETCH_START, fetchFoo);
}
  1. Can I write a function in the component to make the async call?

// React component, I trigger the fetch by an action creation in componentDidMount

class Foo extends React.Component {
  componentDidMount() {
    this.props.startFetch();
  }
  render() {
    <div>
     {this.props.foo.data ? this.props.foo.data : 'Loading....'}
    <div>
  }
}
const mapStateToProps = (state) => ({foo: state.foo});
const mapDispatchToProps = { startFetch }
export default connect(mapStateToProps, mapDispatchToProps) (Foo);

//client.js, link up saga, redux, and React Component

const render = App => {
 const sagaMiddleware = createSagaMiddleware();
 const store = createStore(
    combinedReducers,
    initialState,
    composeEnhancers(applyMiddleware(sagaMiddleware))
  );
 store.runSaga(rootSaga);
 return ReactDOM.hydrate(
    <ReduxProvider store={store}>
      <BrowserRouter><AppContainer><App/></AppContainer></BrowserRouter>
    </ReduxProvider>,
    document.getElementById('root')
  );
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM