Author avatar

Manujith Pallewatte

Reusable Components with React and Redux

Manujith Pallewatte

  • Jun 11, 2019
  • 15 Min read
  • 1,809 Views
  • Jun 11, 2019
  • 15 Min read
  • 1,809 Views
Web Development
React

Introduction

Reusable components are at the heart of React. Components come in two flavors:

  1. Presentational or "dumb" components
  2. Container components

Presentational components are, in general, concerned with rendering an HTML UI given input. Input could be static content or dynamic content retrieved from an external API. They don't manage any state except internal state that is required for presentational purposes. Buttons, Lists, Inputs are some examples of these.

Container components, on the other hand, are more robust. They contain some logic apart from the UI functions. With redux managing the app state, we often find it tricky to decide the level of control to give the components. In a perfectly structure app, there should be a good balance between presentational and container components.

To start off, let's have a quick look at what we are building today. We will be using the following npm packages,

1
2
3
4
5
    "react": "^16.8.6",
    "react-dom": "^16.8.6",
    "react-redux": "^7.0.3",
    "react-router-dom": "^5.0.0",
    "redux": "^4.0.1"
js

"Noogle" - The Search Engine

We are tasked with making the front-end for a simple search engine that we’ll call "Noogle". Essentially it has two pages:

  1. Home page: where we only have a long search bar
  2. Results page: where we show search results along with the search bar

As developers, our first step is to carry out the component breakdown of the app. On first glance, we know that we have two pages, ie: two components. We'll call them Page Components.

1
2
3
App
    |- HomePageComponent
    |- ResultsPageComponent

In HomePageComponent, we have a search component which is a text box coupled with a button. ResultsPageComponent has a search as well as a result list component. The result list component is a list of search results. So, now our breakdown comes up as:

1
2
3
4
5
6
App
    |- HomePageComponent
        |- HomeSearchComponent
    |- ResultsPageComponent
        |- ResultsSearchComponent
        |- ResultsListComponent

Now we can observe that both HomeSearchComponent and ResultsSearchComponent in fact have similar usage. Both accept a text input and fire some action on a button click. This is where the power of reusability kicks in. Rather than having two separate components, we reuse a single component in both instances.

1
2
3
4
5
6
App
    |- HomePageComponent
        |- SearchComponent
    |- ResultsPageComponent
        |- SearchComponent
        |- ResultsListComponent

Creating the App

Let's start off by creating a new react app using the React CLI. In your terminal, generate the app with following command:

1
2
$ create-react-app noogle
$ cd noogle
bash

Next we add redux and react-router to our project:

1
$ npm install --save redux react-redux react-router-dom
bash

Once done, we create the two page components along with a simple router for navigating the pages. We initialize the store, actions, and reducer for redux as well. The directory structure for SRCsrc is as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
src
    |- components
        |- SearchComponent.js
    |- pages
        |- HomePageComponent.js
        |- ResultsPageComponent.js
    actions.js
    App.css
    App.js
    index.css
    index.js
    reducers.js
    store.js

To make sure Noogle looks amazing, I cooked up a few CSS additions. You could either use these or be creative yourself. Also, don't forget to link bootstrap to index.html as well.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/** index.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;
}

.homepage{
  width: 800px;
  margin-left: auto;
  margin-right: auto;
  margin-top: 200px;
  text-align: center;
}

.homepage h1{
  margin-bottom: 30px;
}

.homepage input{
  margin-bottom: 15px;
}


.resultspage{
  margin-top: 50px;
}

.resultspage .search{
  margin-bottom: 50px;
  margin-top: 20px;
}

.resultspage input{
  display: inline-block;
  width: 500px;
  margin-right: 15px;
}

.resultspage .result{
  width: 800px;
  border: 1px solid #ccc;
  padding: 10px 10px 10px 10px;
  margin-bottom: 10px;
}

.resultspage .result .title{
  color: #1a0dab;
  font-size: 20px;
}

.resultspage .result .url{
  color: #006621;
}
css

To begin with, let's create a Router to our app to facilitate page transitions. Along with this, we will initialize the redux boilerplate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// HomePageComponent.js

import React, { Component } from 'react';

class HomePageComponent extends Component{
    render(){
        return (
            <div className="container">
                <h1>Home Page</h1>
            </div>
        )
    }
}

export default HomePageComponent;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ResultsPageComponent.js

import React, { Component } from 'react';

class ResultsPageComponent extends Component{
    render(){
        return (
            <div className="container">
                <h1>Results Page</h1>
            </div>
        )
    }
}

export default ResultsPageComponent;
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// reducers.js

const initialState = {

}

function searchReducer(state, action) {
    if (typeof state === 'undefined') {
        return initialState
    }

    // We create an empty reducer for now

    return state
}
jsx
1
2
3
4
5
6
// store.js

import { createStore } from 'redux'
import searchReducer from './reducers'

const store = createStore(todoApp)
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// App.js

import React from 'react';
import { BrowserRouter as Router, Route, Link } from "react-router-dom";

import HomePageComponent from './pages/HomePageComponent';
import ResultsPageComponent from './pages/ResultsPageComponent';

import { Provider } from 'react-redux'
import store from './store';

function App() {
  return (
    <Provider store={store}>
      <Router>
        <div>
          <Route path="/" exact component={HomePageComponent} />
          <Route path="/results" component={ResultsPageComponent} />
        </div>
      </Router>
    </Provider>
  );
}

export default App;
jsx

Now, run the app with $ npm start and test how it looks from the browser. It should show the home page and results page when you navigate to http://localhost:3000 and http://localhost:3000/results respectively.

Building the SearchComponent

The SearchComponent consists of a text input and a button. The goal of the button is to execute a search procedure using the query in the text input. First, we create the SearchComponent inside the components directory. We could use the components directory to store the shared components in the app.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// SearchComponent.js

import React, { Component } from 'react';

class SearchComponent extends Component{
    state = {
        query: ""
    }

    onInputChange = (e) => {
        this.setState({
            query: e.target.value
        })
    }

    onButtonClick = (e) => {
        //Todo: fire the search processes
    }

    render(){
        return (
            <div className="search">
                <input type="text" 
                    value={this.state.query} 
                    onChange={this.onInputChange}
                />
                <button onClick={this.onButtonClick}>Search</button>
            </div>
        )
    }
}

export default SearchComponent;
jsx

In this example, we use a state management pattern where the UI state is managed internally - ie: the state of the input field is managed inside the component. Although we have redux for state management, overcrowding the redux store with states that are not relevant to the overall app state is a mistake. An alternative pattern would be to have a dedicated redux store for UI state handling.

With the component UI done, let's figure out how to make the actual search work.

Making the Search Work

To keep the guide simple and focused, we won't be connecting any real APIs to the app. Rather, we will hardcode some values in our store and use simple text filtering to simulate a search behavior. In simple terms, we will have a fixed array of search items. Once a query is fired, we will filter our search items to match the query. Then we present these items in our results view. The following code modifies the reducer and the actions to add this behavior:

1
2
3
4
5
6
7
8
9
10
11
12
13
// actions.js

// These are our action types
export const SEARCH_LIST = "SEARCH_LIST"


// Now we define actions
export function searchList(query){
    return {
        type: SEARCH_LIST,
        query
    }
}
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// reducers.js 

import { SEARCH_LIST } from './actions';

const initialState = {
    results: []
}

// Mock data
const data = [
    {
        "title": "React – A JavaScript library for building user interfaces",
        "url": "https://reactjs.org/"
    },
    {
        "title": "Tutorial: Intro to React – React",
        "url": "https://reactjs.org/tutorial/tutorial.html"
    },
    {
        "title": "GitHub - facebook/react: A declarative, efficient, and flexible JavaScript",
        "url": "https://github.com/facebook/react"
    },
    {
        "title": "What is React - W3Schools",
        "url": "https://www.w3schools.com/whatis/whatis_react.asp"
    },
]

export default function searchReducer(state, action) {
    if (typeof state === 'undefined') {
        return initialState
    }

    if(action.type === SEARCH_LIST){
        const results = [];
        for(var item of data){
            if(item.title.indexOf(action.query) !== -1){
                results.push(item);
            }
        }

        return {
            ...state,
            results: results,
        }
    }
}
jsx

Now that the search action is available, we need to bind this to our search button inside the SearchComponent. With that, our SearchComponent is complete and, more importantly, it works out-of-the-box. Which means that the component doesn't depend on any prop from the parent component to function properly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// SearchComponent.js

import { connect } from 'react-redux';
import { searchList } from '../actions';

//...

    onButtonClick = (e) => {
        this.props.searchList(this.state.query);
    }
//...

const mapDispatchToProps = {
    searchList,
};

export default connect(null, mapDispatchToProps)(SearchComponent);
jsx

We can simply add the SearchComponent to both HomePageComponent and ResultsPageComponent and verify that it functions as desired. Next, we'll build the ResultsListComponent.

Building the ResultsListComponent

The final piece of this puzzle is to show the search results in results page. For this, we will build a dumb component that accepts an array of items and display them. But the connectivity to the store will not be given to it. The parent component, which is the ResultsPageComponent, would bind to the store and provide the data to ResultsListComponent. This demonstrates the second type of reusability while using redux.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ResultsListComponent.js

import React from 'react';

const ResultsListComponent = (props) => {
    return (
        <div className="result-list">
            {props.results.map((result, index) => (
                <div className="result" key={index}>
                    <div className="title">{result.title}</div>
                    <div className="url">{result.url}</div>
                </div>
            ))}
        </div>
    )
}

export default ResultsListComponent;
jsx

We’ll update the ResultsPageComponent to use our shiny new component and attach the store to pass on the results data as necessary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// ResultsPageComponent.js

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

class ResultsPageComponent extends Component{
    render(){
        return (
            <div className="container">
                <h1>Results Page</h1>
                <ResultsListComponent results={this.props.results} />
            </div>            
        )
    }
}

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

export default connect(mapStateToProps, null)(ResultsPageComponent);
jsx

Route Redirection with react-redux

Now our brand new search engine is ready to function; one small issue is that when we do a search from the HomePageComponent, it should redirect to the results page for us to see the results. In react-redux combination, doing route changes after an action is a tricky situation. Since redux actions must be side-effect free, it's highly recommended not to have route changes within that data flow. Instead, we use several "hacks". In this guide I will briefly show one such method, but will explain all different methods at another time. These edits to the reducer and HomePageComponent will provide the desired routing as necessary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// reducers.js

// ...
const initialState = {
    results: [],
    hasResultsLoaded: false
}
//...
    return {
        ...state,
        results: results,
        hasResultsLoaded: true
    }
//...
jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react';
import SearchComponent from '../components/SearchComponent';
import { connect } from 'react-redux';
import { withRouter } from "react-router-dom";

class HomePageComponent extends Component{

    shouldComponentUpdate(next, prev){
        if(next.hasResultsLoaded === true){
            this.props.history.push("/results");
            return false;
        }

        return true;
    }

    render(){
        return (
            <div className="container">
                <h1>Home Page</h1>
                <SearchComponent />
            </div>
        )
    }
}

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

export default connect(mapStateToProps, null)(withRouter(HomePageComponent));
jsx

Conclusion

Our little search engine comes to a conclusion, at this point. You can pump it up a bit by adding better styling, search query to path parameters, and so on. The key takeaway of this guide is the understanding of how component reusability works when redux is in the play. Most of us get stuck in ideation when trying to figure out whether to use redux or component state in managing reusable components. As shown above, it can be done either way or as a mix, but knowing the options will give you a better space to think.

7