DEV Community

Cover image for Structuring React application for scale (Part II)
Anish Kargaonkar
Anish Kargaonkar

Posted on • Edited on

Structuring React application for scale (Part II)

In the last tutorial, we defined the basic structure for a scaleable react application. To demonstrate how it all comes together, we are going to build a Reddit client where a user can search about multiple topics and get results in form of a list.

In case if you did not already, please refer to Part I to understand the structure in depth.

srp-optimize

Github: https://github.com/anishkargaonkar/react-reddit-client
Hosted On: https://reddit-client-88d34d.netlify.app/

The Reddit Client

Let's start by creating a container called Search at /src/cotainers/Search/Search.tsx

// /src/cotainers/Search/Search.tsx
import React, { FC } from "react";

type Props = {};

const Search: FC<Props> = (props: Props) => {
    return (
        <div>Search Container</div>
    )
};

export { Search };
Enter fullscreen mode Exit fullscreen mode

and add it to the Navigator component at /src/navigator/Navigator.tsx

// /src/navigator/Navigator.tsx
import React, { FC } from "react";
import { Switch, Route, BrowserRouter as Router } from "react-router-dom";
import { Search } from "../containers/Search/Search";

type Props = {};

const Navigator: FC<Props> = () => {
  return (
    <Router>
      <Switch>
        <Route path="/" component={Search} />
      </Switch>
    </Router>
  );
};

export { Navigator };
Enter fullscreen mode Exit fullscreen mode

After doing the above changes, the folder structure should look something like this

Untitled

Adding search state

We'll be using Reddit's search API to query and fetch results. The format is given below

https://www.reddit.com/r/all/search.json?q=<query>&limit=<limit>
Enter fullscreen mode Exit fullscreen mode

You can find more details on Reddit's official documentation

Let's define our API endpoints in .env

// /.env

REACT_APP_PRODUCTION_API_ENDPOINT = "https://www.reddit.com"
REACT_APP_DEVELOPMENT_API_ENDPOINT = "https://www.reddit.com"
Enter fullscreen mode Exit fullscreen mode

In our case, both endpoints are going to be the same as we do not have separate environments for our app's back-end.

Before defining our redux state first we need to know how would our data looks, so let's first define the model by creating a file types.ts in our Search container.

Generally, these models are decided early on before starting the project which off-course evolves over a period of time. Sometimes it might happen that we don't have a model beforehand and in that case, the developer is free to use his/her imagination based on the use case. But it's better to start after having a starting point which helps to avoid a lot of changes in later stages. For our use case, we can make a query to the above search query link to get the response and use a typescript generator tool like json2ts to get our typescript schema.

Note: If you are using JavaScript, you can skip this part but do take a look at the model once.

// src/containers/Search/types.ts
export interface Result {
  title: string;
  thumbnail: string;
  permalink: string;
}

export interface SearchResults {
  after: string;
  dist: number;
  modhash: string;
    children: {
        kind: string;
        data: Result;
  };
  before?: any;
}

// reddit API response Model
export interface Search {
  kind: string;
  data: SearchResults;
}
Enter fullscreen mode Exit fullscreen mode

We have defined a model called Search which represents the data sent from the Reddit search API. To keep it simple we've omitted attributes that are not used in the app. Result model represents each Reddit result.

We'll also add a SearchQuery interface in types.ts where we will define query parameters required to make a Reddit search

// src/containers/Search/types.ts

... // Search Result model

export interface SearchQuery {
  query: string;
  limit: number;
};
Enter fullscreen mode Exit fullscreen mode

Now let's define the redux state and actions types for Search container in types.ts

// src/containers/Search/types.ts
import { CustomError } from "../../utils/api-helper";

... // Search Result interface 

... // Search Query interface

// Search action types
export enum SearchActionTypes {
    GET_RESULTS_REQUEST = "@@search/GET_RESULTS_REQUEST",
    GET_RESULTS_SUCCESS = "@@search/GET_RESULTS_SUCCESS",
    GET_RESULTS_ERROR = "@@search/GET_RESULTS_ERROR",  
}

interface Errors {
  results: CustomError | null
}

// Search redux state 
export interface SearchState {
   isLoading: boolean,
   results: Search | null,
   errors: Errors
}
Enter fullscreen mode Exit fullscreen mode

For search API requests there can only be 3 states at any given point of time .i.e.

  • GET_RESULTS_REQUEST: while fetching results
  • GET_RESULTS_SUCCESS: when we receive a successful response
  • GET_RESULTS_ERROR: when we receive an error response

Similarly, for the Search container state we've defined

  • isLoading: boolean to keep a track if any API request is being made or not
  • results: where search results are going to be stored.
  • errors: where at most 1 error response for each attribute will be tracked (here we are tracking for results).

If you would have noticed we are using a pipe( | ) operator with null type which means that at any given point it's value will be either of type T or null. We can also use undefined but this way we'll need to always declare that attribute and assign a null value which in turn makes our code more readable.

Let's also add SearchState to the ApplicationState defined in src/store.ts and call it search

// src/store.ts
... // imports
import { SearchState } from './containers/Search/reducer';

export type ApplicationState = {
  search: SearchState
};

function configureAppStore(initialState: ApplicationState) {
  ... // store configuration 
}

export { configureAppStore };
Enter fullscreen mode Exit fullscreen mode

Let's define actions for Search state in redux. For this, we are going to use redux-toolkit's createAction and createReducer helper functions for actions and reducer respectively.

// src/containers/Search/action.ts
import { createAction } from "@reduxjs/toolkit";
import { CustomError } from "../../utils/api-helper";
import { Search, SearchActionTypes, SearchQuery } from "./types";

export const getResultsRequest = createAction<SearchQuery>(
  SearchActionTypes.GET_RESULTS_REQUEST
);

export const getResultsSuccess = createAction<Search>(
  SearchActionTypes.GET_RESULTS_SUCCESS
);

export const getResultsError = createAction<CustomError>(
  SearchActionTypes.GET_RESULTS_ERROR
);
Enter fullscreen mode Exit fullscreen mode

Here we have defined 3 action types. Since we are using Typescript, we have also defined the payload type for getResultsRequest getResultsSuccess and getResultsError. The payload type will help connect the flow and avoid errors.

It's time to setup reducer for the Search state which will listen to dispatched action and if the action type matches, the redux state will be updated. To create the reducer, we are going to use the createReducer helper utility from redux-toolkit using builder callback notation which is recommended with Typescript. For more information feel free to check the redux-toolkit docs.

// src/containers/Search/reducer.ts
import { createReducer } from "@reduxjs/toolkit";
import {
  getResultsError,
  getResultsRequest,
  getResultsSuccess,
} from "./action";
import { SearchState } from "./types";

const initalState: SearchState = {
  isLoading: false,
  results: null,
  errors: {
    results: null,
  },
};

const reducer = createReducer(initalState, (builder) => {
  return builder
    .addCase(getResultsRequest, (state, action) => {
      state.isLoading = true;
      state.results = null;
      state.errors.results = null;
    })
    .addCase(getResultsSuccess, (state, action) => {
      state.isLoading = false;
      state.results = action.payload;
    })
    .addCase(getResultsError, (state, action) => {
      state.isLoading = false;
      state.errors.results = action.payload;
    });
});

export { initalState as searchInitialState, reducer as searchReducer };
Enter fullscreen mode Exit fullscreen mode

Here we are creating a reducer that will listen for SearchActionTypes created earlier and update state accordingly. Now for the sake of keeping this example simple, we are not considering pagination and other advance list operations. We'll assume that search results will be only fetched once and we'll keep data for the latest request therefore, we are resetting state when a new getResultsRequest is made. We are also exporting the initial state (searchInitialState) which will also represent the search state when the application is bootstrapped.

NOTE: You can also use createSlice method provided by redux-toolkit which will create both actions as well as a reducer for you. Action types can be provided inline. For more information, you can refer to redux-toolkit docs.

Now let's add the initial search state to the initial application state in src/App.tsx

// src/App.tsx
import React from 'react';
import { Provider } from 'react-redux';
import { ApplicationState, configureAppStore } from './store';
import { Navigator } from "./navigator/Navigator";
import { searchInitialState } from './containers/Search/reducer';

const initialState: ApplicationState = {
  search: searchInitialState;
};

const store = configureAppStore(initialState);

function App() {
  return (
    <Provider store={store}>
      <Navigator />
    </Provider>
  );
}

export default App; 
Enter fullscreen mode Exit fullscreen mode

We also need to add the search reducer in the root reducer by adding it to src/reducer.ts

// src/reducer.ts
import { combineReducers } from "@reduxjs/toolkit";
import { searchReducer } from './containers/Search/reducer';

const reducers = {
  search: searchReducer
};

function createRootReducer() {
    const rootReducer = combineReducers({
      ...reducers
    });

    return rootReducer;
};

export { createRootReducer };
Enter fullscreen mode Exit fullscreen mode

Now when you run the application, you should be able to see a search state available in the redux state.

Untitled 1

The folder structure will look something like this

Untitled 2

Now that we are done with the redux setup, it's time to setup saga middleware for the Search container. Let's start by creating a file saga.ts in the Search container and define a getSearchResults function which will listen for GET_SEARCH_RESULTS action type. In order to understand how redux-saga work you can check out their official docs.

// src/containers/Search/saga.ts

import { all, fork, takeLatest } from "redux-saga/effects";
import { getResultsRequest } from "./action";

function* getSearchResults() {
    // get search results API request
}

function* watchFetchRequest() {
  yield takeLatest(getResultsRequest.type, getSearchResults);
}

export default function* searchSaga() {
  yield all([fork(watchFetchRequest)]);
}
Enter fullscreen mode Exit fullscreen mode

We have defined a searchSaga which we'll import in store.ts so that it is registered. getSearchResults will contain the code responsible for making an API request and depending on the response it'll dispatch a success or error action.

Before that, we'll need to first create a function for making API requests in src/services/Api.ts. As mentioned above, to get search results from Reddit we can use the following endpoint and we'll pass the query & limit from the component.

https://www.reddit.com/r/all/search.json?q=<query>&limit=<limit>
Enter fullscreen mode Exit fullscreen mode

We have already added the base URL (https://www.reddit.com) as API_ENDPOINT in the environment configuration.

Let's define a function fetchSearchResults and we'll use the get helper function from src/utils/api-helper.ts.

// src/services/Api.ts
import config from "../config/app";
import * as API from "../utils/api-helper";
import { SearchQuery } from "../containers/Search/types";

const { isProd } = config;

const API_ENDPOINT = isProd 
    ? config.production 
    : config.development;

export const fetchSearchResults = (params: SearchQuery) => {
  const { query, limit } = params;
  const url = `${API_ENDPOINT}/r/all/search.json?q=${query}&limit=${limit}`;

  return API.get(url);
};
Enter fullscreen mode Exit fullscreen mode

Now we can use fetchSearchResults, let's complete our search saga and make a get search API call.

Specifying the action as an argument to a saga is a bit tricky, we have to use TypeScript's Type Guards. Interestingly, it's mentioned in the redux-toolkit's documentation as well. In short, we have to use actionCreator.match method of the actionCreator to discriminate down the passed action to the desired type. Thus, after discrimination, we receive the desired static typing for the matched action's payload.

After playing around with the response, I ended up with the following saga.ts.

// src/containers/Search/saga.ts
import { Action } from '@reduxjs/toolkit';
import { all, call, fork, put, takeLatest } from "redux-saga/effects";
import { getResultsError, getResultsRequest, getResultsSuccess } from "./action";
import * as Api from "../../services/Api";
import { getCustomError } from '../../utils/api-helper';

function* getSearchResults(action: Action) {
  try {
    if (getResultsRequest.match(action)) {
      const res = yield call(Api.fetchSearchResults, action.payload);
      const data = res.data;
      if (res.status !== 200) {
        yield put(getResultsError(data.error));
      } else {
        yield put(getResultsSuccess(data));
      }
    }
  } catch (err) {
    yield put(getResultsError(getCustomError(err)))
  }
}

function* watchFetchRequest() {
  yield takeLatest(getResultsRequest.type, getSearchResults);
}

export default function* searchSaga() {
  yield all([fork(watchFetchRequest)]);
}
Enter fullscreen mode Exit fullscreen mode

To register searchSaga, simply import it in root saga at src/saga.ts.

// src/saga.ts
import { all, fork } from "redux-saga/effects";
import searchSaga from "./containers/Search/saga";

function* rootSaga() {
    yield all([
        fork(searchSaga)
    ]);
};

export { rootSaga };
Enter fullscreen mode Exit fullscreen mode

This completes the data setup for the application. Now we can start with UI implementation. The folder structure will look something like this

Untitled 3

Setting up the UI

We can divide the UI into 2 parts

  • SearchInput: It'll have an input field which will take in search query from the user
  • Results: Basically here we'll show results from the query

Let's create a folder called views at src/containers/Search/views where the above-listed components will go. The view folder (sometimes named as screens) inside the container will contain components that are specific to that container or accessing the global state (in our case redux state).

For sake of simplicity and since making components such as Input and Loader is outside the scope of this article, I'll be using a components library ant design. But in case you are wondering, components that might be used in multiple places stateless or otherwise will go inside the src/components folder.

Though if you are using hooks, it might be a little difficult to decide where a component should go. In that case, as a thumb rule if a component is accessing the global state .i.e. from the redux store using useSelector hook, then it should be listed under src/containers/{feature}/views folder.

Let's add ant design component to the project

yarn add antd @ant-design/icons
Enter fullscreen mode Exit fullscreen mode

Once the process is complete, we'll need to add ant design's CSS to /src/index.css. Let's use the dark theme because well, who doesn't love a dark theme.

// src/index.css
@import '~antd/dist/antd.dark.css';

body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
    sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
    monospace;
}
Enter fullscreen mode Exit fullscreen mode

Let's create SearchInput component inside src/containers/Search/views where user can search for a topic

// src/containers/Search/views/SearchInput.tsx
import React, { FC, useEffect, useState } from "react";
import { Avatar, Input } from "antd";
import logo from "../../../assets/logo.svg";
import "../styles.css";
import { useDispatch, useSelector } from "react-redux";
import { ApplicationState } from "../../../store";
import { getResultsRequest } from "../action";

type Props = {};

const { Search } = Input;
const SearchInput: FC<Props> = (props: Props) => {
  const dispatch = useDispatch();
  const [searchQuery, setSearchQuery] = useState("");
  const [searchQueryLimit, setSearchQueryLimit] = useState(0);

  const isLoading = useSelector<ApplicationState, boolean>(
    (s) => s.search.isLoading
  );

  const onSearchQueryChangeHandler = (
    e: React.ChangeEvent<HTMLInputElement>
  ) => {
    const val = e.target.value;
    setSearchQuery(val);
  };

  const onSearchHandler = () => {
    dispatch(getResultsRequest({
      query: searchQuery,
      limit: searchQueryLimit
    }))
  }

  useEffect(() => {
    setSearchQueryLimit(25);
  }, [])

  return (
    <div className="search-input-container">
      <Avatar src={logo} shape="circle" size={150} />
      <Search
        className="search-input"
        placeholder="Search for a topic"
        loading={isLoading}
        value={searchQuery}
        onChange={onSearchQueryChangeHandler}
        onSearch={onSearchHandler}
      />
    </div>
  );
};

export { SearchInput };
Enter fullscreen mode Exit fullscreen mode

Let's start from the top, we have created a functional component SearchInput. We are using useSelector and useDispatch hooks to access redux state and dispatch redux actions. We are also using useState hook for managing search query and search query limit locally and useEffect to perform side effects in function components.

From the ant design components library, we have imported Avatar and Input.Search component. We have also defined some styles in src/containers/Search/styles.css and also added Reddit logo SVG in src/assets.

/* src/containers/Search/styles.css */
.container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.search-input-container {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
}

.search-input {
    margin: 2rem 0;
    border-radius: 5px;
}
Enter fullscreen mode Exit fullscreen mode

Now import SearchInput component in Search

// src/containers/Search/Search.tsx

import React, { FC } from "react";
import "./styles.css";
import { SearchInput } from "./views/SearchInput";

type Props = {};

const Search: FC<Props> = (props: Props) => {
  return (
    <div className="container">
      <SearchInput />
    </div>
  );
};

export { Search }; 
Enter fullscreen mode Exit fullscreen mode

Now hit save and let it compile then navigate to http://localhost:3000 you should be able to see something like this

Screenshot_from_2020-09-22_22-55-00

Folder structure so far

Untitled 4

Now let's work on the Results component which will show the results from the query. We'll add this component to the views folder of the Search container.

Let's create a custom component called ResultListItem to display each result. Also, let's add an action type to reset the results which we can use to get back to the starting screen.

// src/containers/Search/types.ts

// ... SearchResults model

export interface Search {
  kind: string;
  data: SearchResults;
}

export interface SearchQuery {
  query: string;
  limit: number;
};

interface Errors {
  results: CustomError | null
}

export enum SearchActionTypes {
  GET_RESULTS_REQUEST = "@@search/GET_RESULTS_REQUEST",
  GET_RESULTS_SUCCESS = "@@search/GET_RESULTS_SUCCESS",
  GET_RESULTS_ERROR = "@@search/GET_RESULTS_ERROR",

  **RESET_RESULTS = '@@search/RESET_RESULTS'**
}

export interface SearchState {
  isLoading: boolean,
  results: Search | null,
  errors: Errors
}
Enter fullscreen mode Exit fullscreen mode

Here we are adding a RESET_RESULTS action type to src/containers/Search/types.ts which will be used to reset results state to null in SearchState.

// src/containers/Search/action.ts

import { createAction } from "@reduxjs/toolkit";
import { CustomError } from "../../utils/api-helper";
import { Search, SearchActionTypes, SearchQuery } from "./types";

export const getResultsRequest = createAction<SearchQuery>(
  SearchActionTypes.GET_RESULTS_REQUEST
);

export const getResultsSuccess = createAction<Search>(
  SearchActionTypes.GET_RESULTS_SUCCESS
);

export const getResultsError = createAction<CustomError>(
  SearchActionTypes.GET_RESULTS_ERROR
);

**export const resetResults = createAction(
  SearchActionTypes.RESET_RESULTS
);**
Enter fullscreen mode Exit fullscreen mode

Here we add a new action type resetResults, notice that we have not defined a return type as we have done for other actions? Since there's no value returned in resetResultst there's no need to define an action type.

// src/containers/Search/reducer.ts

import { createReducer } from "@reduxjs/toolkit";
import {
  getResultsError,
  getResultsRequest,
  getResultsSuccess,
  resetResults,
} from "./action";
import { SearchState } from "./types";

const initalState: SearchState = {
  isLoading: false,
  results: null,
  errors: {
    results: null,
  },
};

const reducer = createReducer(initalState, (builder) => {
  return builder
    .addCase(getResultsRequest, (state, action) => {
      state.isLoading = true;
      state.results = null;
      state.errors.results = null;
    })
    .addCase(getResultsSuccess, (state, action) => {
      state.isLoading = false;
      state.results = action.payload;
    })
    .addCase(getResultsError, (state, action) => {
      state.isLoading = false;
      state.errors.results = action.payload;
    })
    .addCase(resetResults, (state, action) => {
      state.results = null;
    });
});

export { initalState as searchInitialState, reducer as searchReducer };
Enter fullscreen mode Exit fullscreen mode

Adding a case for resetResults in the reducer and set results to null .i.e. initial state.

Now let's create a Results component to display search results.

// src/containers/Search/views/Results.tsx
import React, { FC } from "react";
import { useDispatch, useSelector } from "react-redux";
import { ApplicationState } from "../../../store";
import { Search } from "../types";
import { ResultListItem } from "../../../components/ResultListItem/ResultListItem";
import logo from "../../../assets/logo.svg";
import { ArrowLeftOutlined } from "@ant-design/icons";
import { Button } from "antd";
import { resetResults } from "../action";
import "../styles.css";

type Props = {};

const Results: FC<Props> = (props: Props) => {
  const dispatch = useDispatch();
  const results = useSelector<ApplicationState, Search | null>(
    (s) => s.search.results
  );

  const onResetResultsHandler = () => {
    dispatch(resetResults());
  };

  return (
    <div>
      <div className="result-header">
        <Button
          icon={<ArrowLeftOutlined />}
          shape="circle-outline"
          onClick={() => onResetResultsHandler()}
        />
        <div>Search Results</div>
        <div />
      </div>
      {!results || results.data.children.length === 0 ? (
        <div className="no-results-container">No results found</div>
      ) : (
        <div className="results-container">
          {results.data.children.map((result, index) => (
            <ResultListItem
              key={index}
              title={result.data.title}
              imageURL={result.data.thumbnail === "self" ? logo : result.data.thumbnail}
              sourceURL={result.data.permalink}
            />
          ))}
        </div>
      )}
    </div>
  );
};

export { Results };
Enter fullscreen mode Exit fullscreen mode
/* src/containers/Search/styles.css */
.container {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
}

.search-input-container {
    display: flex;
    justify-content: center;
    align-items: center;
    flex-direction: column;
}

.search-input {
    margin: 2rem 0;
    border-radius: 5px;
}

.result-header {
   font-size: 1.5rem;
   display: flex;
   justify-content: space-between;
   align-items: center;
   padding: 0.5rem;
}

.result-header > i {
    cursor: pointer;
}

.results-container {
    max-width: 100vh;
    max-height: 80vh;
    overflow-y: scroll;
}

.no-results-container {
    width: 100vh;
    height: 80vh;
    overflow: hidden;
    display: flex;
    justify-content: center;
    align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

Above we have defined a functional component called Results and the styles are defined in src/containers/Search/styles.css. We are using hooks for getting and resetting redux state results.

Let us now define ResultListItem component and it's styles in src/components/ResultListItem . The pattern followed here is similar to that of the container. For a component that can be used in multiple places, we define it in a folder called components and create a folder with a component name that will contain it's component logic and styles.

// src/components/ResultListItem/ResultListItem.tsx

import React, { FC } from "react";
import "./styles.css";
import logo from "../../assets/logo.svg";

type Props = {
  title: string;
  imageURL: string;
  sourceURL: string;
};

const ResultListItem: FC<Props> = (props: Props) => {
  const { title, imageURL, sourceURL } = props;

  const onClickHandler = (url: string) => {
    window.open(`https://reddit.com/${url}`);
  };

  return (
      <div className="item-container" onClick={() => onClickHandler(sourceURL)}>
          <img className="thumbnail" alt="" src={imageURL} onError={() => logo} />
          <div>
              <div className="title">{title}</div>
          </div>
    </div>
  );
};

export { ResultListItem };
Enter fullscreen mode Exit fullscreen mode
/* src/components/ResultListItem/styles.css */
.item-container {
    display: flex;
    align-items: center;
    padding: 0.5rem;
    width: 100%;
    height: 6rem;
    border: 1px solid rgb(77, 77, 77);
    margin-bottom: 0.5rem;
    border-radius: 4px;
    cursor: pointer;
}

.thumbnail {
    width: 5rem;
    border-radius: 0.2rem;
}

.title {
    font-weight: bold;
    padding: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

And make the following changes to Search container to display Results component if search results are present else show SearchInput component.

// src/containers/Search/Search.tsx
import { message } from "antd";
import React, { FC, useEffect } from "react";
import { useSelector } from "react-redux";
import { ApplicationState } from "../../store";
import { CustomError } from "../../utils/api-helper";
import "./styles.css";
import { Search as SearchModel } from "./types";
import { Results } from "./views/Results";
import { SearchInput } from "./views/SearchInput";

type Props = {};

const Search: FC<Props> = (props: Props) => {
  const results = useSelector<ApplicationState, SearchModel | null>(
    (s) => s.search.results
  );
  const searchError = useSelector<ApplicationState, CustomError | null>(
    (s) => s.search.errors.results
  );

  useEffect(() => {
    if (searchError) {
      message.error(searchError.message);
    }
  }, [searchError]);

  return (
    <div className="container">{!results ? <SearchInput /> : <Results />}</div>
  );
};

export { Search };

Enter fullscreen mode Exit fullscreen mode

Finally, your project structure should look something like this with all the above changes

Untitled 5

Once all the above changes are saved, the project should compile and you should be able to search for a topic and see results as shown below

srp-optimize

You can refer to the following repository for the final code.

GitHub logo anishkargaonkar / react-reddit-client

Reddit client for showing top results for given keywords

Closing thoughts

In this 2 part series, I've tried to define a structure that has worked for me with mid/large scale projects where debugging bugs, adding new features with the ever-changing scope was easy and manageable both in React and React-Native. Though there's no perfect structure that works for all, this can be a good starting point.

I hope you enjoyed reading the article as much as I enjoyed writing it. Would love to hear your thoughts about it. Adios!

Top comments (2)

Collapse
 
sendy profile image
Sandeep

Wow! Great tips.
I would also like to suggest Absolute imports because that makes the imports a lot easier.

Collapse
 
anishkargaonkar profile image
Anish Kargaonkar

Great suggestion 👍