Cookies
Diese Website verwendet Cookies und ähnliche Technologien für Analyse- und Marketingzwecke. Durch Auswahl von Akzeptieren stimmen Sie der Nutzung zu, alternativ können Sie die Nutzung auch ablehnen. Details zur Verwendung Ihrer Daten finden Sie in unseren Datenschutz­hinweisen, dort können Sie Ihre Einstellungen auch jederzeit anpassen.
Engineering

Fetching Data in React Apps

Minuten Lesezeit

Notice: This article is written in German.

Blog Post - Fetching Data in React Apps
Alexander Kott

The Obvious Way: Fetch it where you need it

The solution you'll most commonly see is to write requests directly into the component. A get-request can easily be done from the componentDidMount method.

class UserProfile extends React.Component {
// ...
  async componentDidMount() {
    const { data } = await httpAdapter.get(`${API_URL}/users/${this.props.userId}`);
    this.setState({ user: data.user });
  }
// ...
}

This is a fairly straightforward way, and we'll be able to use the data immediately inside our component through this.state.user. But what if we need the same data in a different part of our app? We might end up fetching the same data in multiple components, even if it should already be available.

The DRY Way: Fetch it once, use it everywhere

Redux is not only great for managing your app state, it might also prevent sending unnecessary requests by providing the fetched data to all components that need it.

The typical way of fetching data in Redux apps is the creation of a Thunk. It allows us to dispatch actions after we've done something else, e.g. sending a request.

export const fetchUser = userId => async dispatch => {
  const { data } = await httpAdapter.get(`${API_URL}/users/${this.props.userId}`);

  dispatch({
    type: 'FETCHED_USER',
    payload: { user: data.user },
  });
};

A reducer can catch this action and store the user data for all components. We still can dispatch the fetchUser action from componentDidMount, but most of the time it is better to fetch the data per route. At Zweitag we like to use Redux First Router, with its onBeforeChange option to define the data needed for each route.

The Zweitag Way: Do it Smart

While dispatching a thunk might be a good fit for apps which only need to fetch data every now and then, it doesn't scale very well. This becomes obvious as soon as we want to implement error handling or a loading state. With the solution above we'd have to wire up every action with a try/catch statement and additional dispatches for changing the loading state.

Wouldn't it be great if we only had to dispatch actions like this:

export const fetchUser = userId => ({
  type: 'SEND_REQUEST',
  payload: {
    name: 'users',
    entity: userId,
  },
});

This action doesn't know any details about the actual request, errors or loading state. It doesn't even describe what happens with the fetched data. Everything of importance can happen in its own module, a Redux middleware.

There are some request middlewares out there which might work for this, but at Zweitag we felt it would be better to keep it small and adjust it to specific needs of each app. The foundation always looks like this:

export default store => next => async action => {
  if (action.type === 'SEND_REQUEST') {
    const {
      method = 'get',
      name,
      entity,
    } = action.payload;
    const endpoint = utils.getEndpoint(name, entity);

    try {
      action.payload.responseData = await httpAdapter[method](endpoint);
    } catch(error) {
      action.payload.error = error;
    }
  }
  next(action);
}

A successful action gets a responseData attached and will be forwarded to the reducers. There you could check for responsibilities:

export default (state = {}, action) => {
  const { type, payload } = action;

  if (
    type === 'SEND_REQUEST' &&
    payload.name === 'users' &&
    !payload.error
  ) {
    return {
      ...state,
      ...payload.responseData,
    };
  }

  return state;      
};

Fetching more data for our app will be as simple as adding a new (small) action, and setting up a reducer to store the response data.

Want More?

With that barebones request middleware we can go ahead and enhance it with all kinds of functionality.

  • Error handling can happen through another middleware which is chained after the request. It only has to listen for action.payload.error and react accordingly.
  • To handle the loading state another middleware can listen to SEND_REQUEST coming through and dispatch SET_LOADING actions.
  • Let's say you want to react to a request not only in the reducer, but by dispatching another action. We can add onSuccess methods to the request action and dispatch them from the middleware.
  • Even more, you can add onError and onFinally for maximum flexibility.

At Zweitag we often use a different combination of all these features, so that we have a custom middleware with all the things we need without bloating the code. That way we can improve on this functionality while developing exactly what the app needs.

Partner für digitale Geschäftsmodelles

Are you looking for the right partner for your digital projects?

Let's talk!