Reusable components are at the heart of React. Components come in two flavors:
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 "react": "^16.8.6",
2 "react-dom": "^16.8.6",
3 "react-redux": "^7.0.3",
4 "react-router-dom": "^5.0.0",
5 "redux": "^4.0.1"
We are tasked with making the front-end for a simple search engine that we’ll call "Noogle". Essentially it has two pages:
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.
1App
2 |- HomePageComponent
3 |- 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:
1App
2 |- HomePageComponent
3 |- HomeSearchComponent
4 |- ResultsPageComponent
5 |- ResultsSearchComponent
6 |- 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.
1App
2 |- HomePageComponent
3 |- SearchComponent
4 |- ResultsPageComponent
5 |- SearchComponent
6 |- ResultsListComponent
Let's start off by creating a new react app using the React CLI. In your terminal, generate the app with following command:
1$ create-react-app noogle
2$ cd noogle
Next we add redux and react-router to our project:
1$ npm install --save redux react-redux react-router-dom
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.
1src
2 |- components
3 |- SearchComponent.js
4 |- pages
5 |- HomePageComponent.js
6 |- ResultsPageComponent.js
7 actions.js
8 App.css
9 App.js
10 index.css
11 index.js
12 reducers.js
13 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/** index.css **/
2
3body {
4 margin: 0;
5 font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
6 "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
7 sans-serif;
8 -webkit-font-smoothing: antialiased;
9 -moz-osx-font-smoothing: grayscale;
10}
11
12code {
13 font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
14 monospace;
15}
16
17.homepage{
18 width: 800px;
19 margin-left: auto;
20 margin-right: auto;
21 margin-top: 200px;
22 text-align: center;
23}
24
25.homepage h1{
26 margin-bottom: 30px;
27}
28
29.homepage input{
30 margin-bottom: 15px;
31}
32
33
34.resultspage{
35 margin-top: 50px;
36}
37
38.resultspage .search{
39 margin-bottom: 50px;
40 margin-top: 20px;
41}
42
43.resultspage input{
44 display: inline-block;
45 width: 500px;
46 margin-right: 15px;
47}
48
49.resultspage .result{
50 width: 800px;
51 border: 1px solid #ccc;
52 padding: 10px 10px 10px 10px;
53 margin-bottom: 10px;
54}
55
56.resultspage .result .title{
57 color: #1a0dab;
58 font-size: 20px;
59}
60
61.resultspage .result .url{
62 color: #006621;
63}
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// HomePageComponent.js
2
3import React, { Component } from 'react';
4
5class HomePageComponent extends Component{
6 render(){
7 return (
8 <div className="container">
9 <h1>Home Page</h1>
10 </div>
11 )
12 }
13}
14
15export default HomePageComponent;
1// ResultsPageComponent.js
2
3import React, { Component } from 'react';
4
5class ResultsPageComponent extends Component{
6 render(){
7 return (
8 <div className="container">
9 <h1>Results Page</h1>
10 </div>
11 )
12 }
13}
14
15export default ResultsPageComponent;
1// reducers.js
2
3const initialState = {
4
5}
6
7function searchReducer(state, action) {
8 if (typeof state === 'undefined') {
9 return initialState
10 }
11
12 // We create an empty reducer for now
13
14 return state
15}
1// store.js
2
3import { createStore } from 'redux'
4import searchReducer from './reducers'
5
6const store = createStore(todoApp)
1// App.js
2
3import React from 'react';
4import { BrowserRouter as Router, Route, Link } from "react-router-dom";
5
6import HomePageComponent from './pages/HomePageComponent';
7import ResultsPageComponent from './pages/ResultsPageComponent';
8
9import { Provider } from 'react-redux'
10import store from './store';
11
12function App() {
13 return (
14 <Provider store={store}>
15 <Router>
16 <div>
17 <Route path="/" exact component={HomePageComponent} />
18 <Route path="/results" component={ResultsPageComponent} />
19 </div>
20 </Router>
21 </Provider>
22 );
23}
24
25export default App;
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.
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// SearchComponent.js
2
3import React, { Component } from 'react';
4
5class SearchComponent extends Component{
6 state = {
7 query: ""
8 }
9
10 onInputChange = (e) => {
11 this.setState({
12 query: e.target.value
13 })
14 }
15
16 onButtonClick = (e) => {
17 //Todo: fire the search processes
18 }
19
20 render(){
21 return (
22 <div className="search">
23 <input type="text"
24 value={this.state.query}
25 onChange={this.onInputChange}
26 />
27 <button onClick={this.onButtonClick}>Search</button>
28 </div>
29 )
30 }
31}
32
33export default SearchComponent;
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.
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// actions.js
2
3// These are our action types
4export const SEARCH_LIST = "SEARCH_LIST"
5
6
7// Now we define actions
8export function searchList(query){
9 return {
10 type: SEARCH_LIST,
11 query
12 }
13}
1// reducers.js
2
3import { SEARCH_LIST } from './actions';
4
5const initialState = {
6 results: []
7}
8
9// Mock data
10const data = [
11 {
12 "title": "React – A JavaScript library for building user interfaces",
13 "url": "https://reactjs.org/"
14 },
15 {
16 "title": "Tutorial: Intro to React – React",
17 "url": "https://reactjs.org/tutorial/tutorial.html"
18 },
19 {
20 "title": "GitHub - facebook/react: A declarative, efficient, and flexible JavaScript",
21 "url": "https://github.com/facebook/react"
22 },
23 {
24 "title": "What is React - W3Schools",
25 "url": "https://www.w3schools.com/whatis/whatis_react.asp"
26 },
27]
28
29export default function searchReducer(state, action) {
30 if (typeof state === 'undefined') {
31 return initialState
32 }
33
34 if(action.type === SEARCH_LIST){
35 const results = [];
36 for(var item of data){
37 if(item.title.indexOf(action.query) !== -1){
38 results.push(item);
39 }
40 }
41
42 return {
43 ...state,
44 results: results,
45 }
46 }
47}
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// SearchComponent.js
2
3import { connect } from 'react-redux';
4import { searchList } from '../actions';
5
6//...
7
8 onButtonClick = (e) => {
9 this.props.searchList(this.state.query);
10 }
11//...
12
13const mapDispatchToProps = {
14 searchList,
15};
16
17export default connect(null, mapDispatchToProps)(SearchComponent);
We can simply add the SearchComponent to both HomePageComponent and ResultsPageComponent and verify that it functions as desired. Next, we'll build 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//ResultsListComponent.js
2
3import React from 'react';
4
5const ResultsListComponent = (props) => {
6 return (
7 <div className="result-list">
8 {props.results.map((result, index) => (
9 <div className="result" key={index}>
10 <div className="title">{result.title}</div>
11 <div className="url">{result.url}</div>
12 </div>
13 ))}
14 </div>
15 )
16}
17
18export default ResultsListComponent;
We’ll update the ResultsPageComponent to use our shiny new component and attach the store to pass on the results data as necessary.
1// ResultsPageComponent.js
2
3import React, { Component } from 'react';
4import ResultsListComponent from '../components/ResultsListComponent';
5import { connect } from 'react-redux';
6
7class ResultsPageComponent extends Component{
8 render(){
9 return (
10 <div className="container">
11 <h1>Results Page</h1>
12 <ResultsListComponent results={this.props.results} />
13 </div>
14 )
15 }
16}
17
18function mapStateToProps(state) {
19 return {
20 results: state.results
21 }
22 }
23
24export default connect(mapStateToProps, null)(ResultsPageComponent);
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// reducers.js
2
3// ...
4const initialState = {
5 results: [],
6 hasResultsLoaded: false
7}
8//...
9 return {
10 ...state,
11 results: results,
12 hasResultsLoaded: true
13 }
14//...
1import React, { Component } from 'react';
2import SearchComponent from '../components/SearchComponent';
3import { connect } from 'react-redux';
4import { withRouter } from "react-router-dom";
5
6class HomePageComponent extends Component{
7
8 shouldComponentUpdate(next, prev){
9 if(next.hasResultsLoaded === true){
10 this.props.history.push("/results");
11 return false;
12 }
13
14 return true;
15 }
16
17 render(){
18 return (
19 <div className="container">
20 <h1>Home Page</h1>
21 <SearchComponent />
22 </div>
23 )
24 }
25}
26
27function mapStateToProps(state) {
28 return {
29 hasResultsLoaded: state.hasResultsLoaded
30 }
31}
32
33export default connect(mapStateToProps, null)(withRouter(HomePageComponent));
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.