For the original link, click here

Posted by Aman Khalid on May 30, 2019

If you feel good, like 👍

  1. Start on the board
  2. Actions, data sources and apis
  3. Redux integration
  4. Dynamic UI at scale

This article walks you through the steps to build a large React application. When creating a single page application using React, the code base can easily become cluttered. This makes it difficult to debug the application and even more difficult to update or extend the code base.

There are a number of good libraries in the React ecosystem for managing certain aspects of your application, and this article takes a closer look at some of these aspects. In addition, it lists some good practices to follow from the beginning of the project if you are concerned about extensibility. Speaking of which, let’s take the first step – how to plan ahead.

Start on the board

For the most part, developers tend to skip this step because it has nothing to do with actual coding, but it’s important, and you’ll see why later.

Application planning stage – Why?

When developing software, developers have to deal with a lot of variable parts that can easily go wrong. Since there are so many uncertainties and obstacles, I don’t want to spend too much time on it. This problem can be avoided during the planning phase, when you write down every detail of the application. It’s much easier to predict how long it will take to build these individual little blocks in front of you than to visualize the whole process in your mind.

If you have multiple developers working on the large project, as you will, having this document will make it easier to communicate with each other. In fact, you can assign content from this document to developers, which makes it easier for everyone to know what everyone else is doing.

Finally, thanks to this document, you’ll be well aware of the progress of your project. It is very common for developers to switch from one feature (A) of the application they are developing to another feature (B), and to return to the current feature (A) development much later than they expected.

Step 1: Views and components

We need to determine the look and function of each view within the application. The best way to do this is to use a modeling tool or draw each view of your application on paper, which will give you a good idea of what information and data you are sure you have on each page.

resources

In the above model, you can easily see the application’s parent-child containers. Later, the parent container for these models will be our application’s pages, and the smaller parts will be placed in the Component folder. Once the models are drawn, write the name of the page and component in each of them.

Step 2: Actions and events within the application

After deciding on components, specify the actions to be performed in each component. These actions will be emitted later from these components

On an e-commerce site, there is a list of featured products on its home screen, and each item in the list is a separate component in the project, named ListItem.

resources

Therefore, in this application, the action performed by the Product component is getItems. Some of the other operations on this page might include getUserDetails, getSearchResults, and so on.

The focus is on observing the actions of each component or the user’s interaction with application data. Wherever you modify, read, or delete data, pay attention to the actions of each page.

Step 3: Data and model

Each component of an application has some data associated with it. If multiple components of an application use the same data, it becomes part of the centralized state tree. The state tree will be managed by Redux.

This data is used by multiple components, so when changes are made to this data by one component, other components also update the data.

List this data in your application because it will form the model for your application and create your application’s reducers based on these values.

products: {
  productId: {productId, productName, category, image, price},
  productId: {productId, productName, category, image, price},
  productId: {productId, productName, category, image, price},
}
Copy the code

Consider the e-commerce store example above. The data type used by feature section and New Arrival section is the same, namely product, which will be one of the reducers of this e-commerce application.

After documenting your action plan, the next section covers some of the details needed to set up the data layer of your application.

Actions, Datasource and API

As applications develop iteratively, the Redux Store often has redundant methods and incorrect directory structures that are difficult to maintain or update.

Let’s take a look at refactoring a few things to make sure the Redux Store code stays clean. Making modules more reusable from the get-go can save a lot of trouble, even if it’s tricky to get started.

API design and client application

When setting up the data store, the format of the data received from the API has a big impact on the layout of the Store. Typically, data needs to be formatted before it can be provided to reducers.

There is a lot of debate about what should and shouldn’t be done when designing aN API. Factors such as backend frameworks, application size, and so on further influence API design.

Keep utility functions such as formatters and mappers in separate folders, just as in back-end applications, to ensure they have no side effects — see Javascript pure functions

export function formatTweet (tweet, author, authedUser, parentTweet) {
  const { id, likes, replies, text, timestamp } = tweet
  const { name, avatarURL } = author

  return {
    name,
    id,
    timestamp,
    text,
    avatar: avatarURL,
    likes: likes.length,
    replies: replies.length,
    hasLiked: likes.includes(authedUser),
    parent: !parentTweet ? null : {
      author: parentTweet.author,
      id: parentTweet.id,
    }
  }
}
Copy the code

In the code snippet above, the formatTweet function inserts a new key parent into the tweet object of the front-end application and returns data based on the parameters, leaving the external data unaffected.

You can further this by mapping the data to a predefined object whose structure is specific to your front-end application, and by validating certain keys. Let’s talk about the part responsible for making the API calls.

Datasource design patterns

The sections I describe in this section will use redux Actions to modify the state. Depending on the size of your application (and how much time you have), you can store data in one of two ways.

  • Without Courier
  • With Courier

Without Courier

Storing data in this way requires defining SEPARATE GET, POST, and PUT requests for each model.

In the figure above, each component dispatches an operation that calls a different data store method, which is the updateBlog method of the BlogApi file.

function updateBlog(blog){
   let blog_object = new BlogModel(blog) 
   axios.put('/blog', { ...blog_object })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
}
Copy the code

This method can save time…… First, it also allows you to make changes without worrying too much about side effects, but there is a lot of redundant code and doing bulk updates can be time-consuming.

With Courier

In the long run, this approach makes maintenance or updates easier and keeps the code base clean, thus eliminating the need for repeated calls through AXIos.

However, this method takes time to set up and is less flexible for you. It’s a double-edged sword because it prevents you from doing something unusual.

export default function courier(query, payload) {
   let path = `${SITE_URL}`;
   path += ` /${query.model}`;
   if (query.id) path += ` /${query.id}`;
   if (query.url) path += ` /${query.url}`;
   if (query.var) path += `?${QueryString.stringify(query.var)}`;
   
   return axios({ url: path, ... payload }) .then(response= > response)
     .catch(error= > ({ error }));
}
Copy the code

Here is a basic Courier method that all API handlers can simply call by passing the following variables:

  • Query object, which will contain URL-related details such as the name of the model, query string, and so on.
  • Payload, which contains the header and body of a request.

API calls and in-application operations

One thing that stands out when working with Redux is the use of predefined actions that make data changes more predictable throughout the application.

Although defining a bunch of constants in a large application might seem like a lot of work, Step 2 in the planning phase makes it much easier.

export const BOOK_ACTIONS = {
   GET:'GET_BOOK'.LIST:'GET_BOOKS'.POST:'POST_BOOK'.UPDATE:'UPDATE_BOOK'.DELETE:'DELETE_BOOK',}export function createBook(book) {
   return {
      type: BOOK_ACTIONS.POST,
    	book
   }
}

export function handleCreateBook (book) {
   return (dispatch) = > {
      return createBookAPI(book)
         .then((a)= > {
            dispatch(createBook(book))
         })
         .catch((e) = > {
            console.warn('error in creating book', e);
            alert('Error Creating book')}}}export default {
   handleCreateBook,
}
Copy the code

The code snippet above shows a simple way to mix the methods of our data source createBookAPI with redux actions. The handleCreateBook method can be safely passed to Redux’s Dispatch method.

Also, note that the code above is in the project’s Actions directory, and we can create javascript files containing action names and handlers for various other models of the application as well.

Redux integration

In this section, I’ll systematically discuss how to extend redux’s capabilities to handle more complex application operations. If not implemented well, these things can break the Store model.

Javascript Generator functions solve many of the problems associated with asynchronous programming because they can be started and stopped at will. The Redux Sagas middleware uses this concept to manage impure aspects of an application.

Manage the impure aspects of your application

Consider this scenario. You are asked to develop a real Estate Discovery application. The customer wants to migrate to a new and better site. The REST API is in place, you’ve got the design for every page on Zapier, and you’ve drafted a plan, but the disaster is still there.

The CMS client has been used in their company for a long time, and they are familiar with it, so they don’t want to switch to a new client just for blogging. Also, copying all the old blogs would be a hassle.

Fortunately, the CMS has a readable API that can serve up blog content. Unfortunately, if you write a Courier, the CMS API resides on a different server with a different syntax.

This is an impure aspect of the application, because you are using a new API for simply getting blogs, which can be handled by using React Sagas.

Consider the following figure. We use Sagas to get blogs from the background. That’s the logic of the whole interaction.

Here, the component performs the dispatch operation GET. Requests in blogs and applications using the Redux middleware will be intercepted, and in the background, your generator functions will fetch data from the data store and update the REdux.

Here is an example of what a generator function for blog Sagas looks like. You can also use Sagas to store user data (such as auth tokens), because this is another impure operation.

. function* fetchPosts(action) {if (action.type === WP_POSTS.LIST.REQUESTED) {
   try {
     const response = yield call(wpGet, {
       model: WP_POSTS.MODEL,
       contentType: APPLICATION_JSON,
       query: action.payload.query,
     });
     if (response.error) {
       yield put({
         type: WP_POSTS.LIST.FAILED,
         payload: response.error.response.data.msg,
       });
       return;
     }
     yield put({
       type: WP_POSTS.LIST.SUCCESS,
       payload: {
         posts: response.data,
         total: response.headers['x-wp-total'].query: action.payload.query,
       },
       view: action.view,
     });
   } catch (e) {
     yield put({ type: WP_POSTS.LIST.FAILED, payload: e.message }); }}...Copy the code

It listens for operations of type wp_posts.list and then gets the data from the API. It assigns another action, wp_posts.list.success, and then updates the blog reducer.

Reducer Injections

For a large application, it is impossible to plan each model up front, and as the application is developed iteratively, this technique saves a lot of effort and allows the developer to add a new Reducer without rewiring the entire store.

There are libraries that let you do this right away, but I prefer this approach because you have the flexibility to integrate it with old code without too much rewiring.

This is a form of code splitting that the community is actively adopting. I’ll use this snippet as an example to show what the Reducer injector looks like and how it works. Let’s first look at how it integrates with Redux.

. const withConnect = connect( mapStateToProps, mapDispatchToProps, );const withReducer = injectReducer({
 key: BLOG_VIEW,
 reducer: blogReducer,
});

class BlogPage extends React.Component {... }export default compose(
 withReducer,
 withConnect,
)(BlogPage);
Copy the code

The code above is part of blogPage.js, which is a component of our application.

Instead of connect, we’re exporting compose, which is another function in the Redux library, and what it does is it allows you to pass multiple functions that can be read from left to right or from bottom to top.

All compose does is let you write deeply nested function transformations without the rightward drift of the code. Don’t give it too much credit!

(From Redux Documentation)

The leftmost function can take multiple arguments, but after that only one argument is passed to the function. Finally, the signature of the rightmost function is used. This is why we pass withConnect as the last parameter, so that the combination can be used just like connect.

Routing and story

Developers like to use a number of tools in their applications to handle routing, but in this section, I’ll stick with the React Router DOM and extend its functionality to use Redux.

The most common way to use a React Router is to wrap the root component with the BrowserRouter tag and the child containers with the withRouter() method and export them [example].

In this way, the child component receives a History Prop with some properties specific to the user session and some methods that can be used to control navigation.

Since there is no central view of the History object, implementing it this way can cause problems in large applications. In addition, components that are not rendered by the Route component cannot access it:

<Route path="/" exact component={HomePage} />
Copy the code

To overcome this problem, we’ll use the Connected React Router library, which allows you to easily use routes through the Dispatch method. This integration point requires some modification, namely creating a new Reducer (obvious) specifically for the routes and adding a new middleware.

After the initial setup, it can be used using Redux, and the in-app navigation can be accomplished simply by using the Dispatching operation.

To use the Connected React Router in the component, we can simply map the Dispatch method to the Store based on your routing needs. Here is a snippet that shows how the Connected React Router is used (you need to make sure the initial setup is complete).

import { push } from 'connected-react-router'. const mapDispatchToProps =dispatch= > ({
  goTo: payload= >{ dispatch(push(payload.path)); }});class DemoComponent extends React.Component {
  render() {
    return (
      <Child 
        onClick={() = >{ this.props.goTo({ path: `/gallery/`}); }} />)}}...Copy the code

In the above code example, the Dispatches operation in the goTo method pushes the URL you want in the browser’s history stack. Since the goTo method is mapped to store, it is passed a property to DemoComponent.

Dynamic UI at scale

Sometimes, despite adequate back-end and core SPA logic, certain elements of the user interface end up compromising the entire user experience because of the rough implementation of components that appear very basic on the surface. In this section, I’ll discuss best practices for implementing some widgets that can get tricky as applications scale.

Soft Loading and Suspense

The best thing about the asynchronous nature of javascript is that you can take full advantage of the browser’s potential. It’s really a good thing that you don’t have to wait for a process to complete before you queue up for a new process. However, as developers, we have no control over the network and the resources loaded on it.

In general, the network layer is considered unreliable and error-prone, and no matter how many times your single-page application passes quality checks, there are some things we can’t control, such as connectivity, response time, etc.

But software developers have put aside the “that’s not my job” mentality and developed elegant solutions to deal with such problems.

In some parts of the front-end app, you’ll want to display some fallback content (something lighter than what you’re trying to load) so the user doesn’t see post-load delay jitter, or worse, this sign:

React Suspense lets you do this by showing some type of loading effect when loading content. Although this can be done by changing the isLoaded property to true, using Suspense is much cleaner.

Learn more about how to use it here. In this video, Jared Palmer takes a look at React Suspense and some of its features in action.

Don’t use Suspense for effects

It is much easier to add suspense to components than to manage isLoaded objects in global state. We first wrap the parent container with React.StrictMode to ensure that any React modules used in the application are not deprecated.

<React.Suspense fallback={<Spinner size="large" />}>
  <ArtistDetails id={this.props.id}/>
  <ArtistTopTracks />
  <ArtistAlbums id={this.props.id}/>
</React.Suspense>
Copy the code

Components wrapped in the React.suspense tag load components specified in their fallback attribute when loading the main content, ensuring that components in the fallback attribute are lightweight.

Use Suspense for effects presentation

Adaptive component

In a large front-end application, repeated patterns begin to appear, even though they may not be as obvious as they started out. You can’t help feeling like you’ve done this before.

For example, in the application you are building there are two models: the track and the car. The car list page has square tiles with an image and some description on each tile.

However, the track list page has an image and description, as well as a small box indicating whether the track is serviced.

The two components above are a little different in style (background color), while the track has additional information. In this example, there are only two models, whereas in a large application, there are many components, and it is counterintuitive to create separate components for each.

By creating adaptive components that understand the context in which they are loaded, you can avoid rewriting similar code. Consider your application’s search bar.

It will be used across multiple pages of your application, with slightly different functions and looks. For example, it will be slightly larger on the home page, and to handle this, you can create a separate component that will render based on the properties passed to it.

static propTypes = {
  open: PropTypes.bool.isRequired,
  setOpen: PropTypes.func.isRequired,
  goTo: PropTypes.func.isRequired,
};
Copy the code

Using this method, you can also switch HTML classes in these components and control their appearance.

Another good example of an adaptive component that you can use is the paging assistant, which is more or less identical to almost every page of your application.

If your API follows a constant design pattern, the only properties you need to pass to the adaptive paging component are the URL and items per page.

conclusion

Over the years, the React ecosystem has matured to such an extent that there is little need to reinvent the wheel at any stage of development. While very useful, it leads to more complexity for your project in choosing what is right.

Each project is different in size and functionality. No single approach or generalization will work every time, so it is essential to have a plan in place before the actual coding begins.

In doing so, it’s easy to identify which tools are right for you and which are redundant. An application with 2-3 pages and minimal API calls does not require the complex data store discussed above. What I want to say is that small projects don’t need to use Redux.

When we plan ahead and analyze and map out the components that will appear in the application, we can see that there is a lot of overlap between pages, and we can save a lot of development costs simply by reusing code or writing flexible components.

Finally, I want to say that data is the backbone of every software project, and this is true for the React application as well. As applications develop iteratively, programmers can easily be overwhelmed by the volume of data and the operations associated with it. Identifying concerns up front such as data storage, reducers Actions, SAGas, etc., can prove to be a huge advantage and make writing them more fun.

If you think there are other libraries or methods that could prove useful when creating large React applications, let us know in the comments. Hope you enjoyed this article and thanks for reading it.

Part of the title in the article has not been translated, so I can’t think of a better translation for the moment. If you have good suggestions, please let me know in the comment section. If there are other unreasonable translations of this article, you can also let me know in the comment section.