Aquasar
  • Home
  • Portfolio
  • Articles
  • Pricing
  • About
  • Contact
WEB DEVELOPMENT |SEO |DIGITAL ADS

Google Custom Search in a React/Redux app

Jun 28th, 2019

Alex Quasar

reactreduxgoogle custom searchsearch in react

If you are looking for a simple to implement website search on your site, that is very easy to set up and handled completely by someone else, ie. Google, then Google Custom Search might be a good solution. While you can directly get the code through this site: https://cse.google.com/cse/all, I choose to go through the API route for three main reasons.

1. I can completely customize the front end UI. In fact, it does not even look like it is coming from Google on the front end. If you use the code above, it will be very clear that it is google powered.

2. I can store the request in a Redux state and even a database, which could be important for various business or UX purposes.

3. No Ads. By that I mean no ads while people are searching on your website. This is important as you want to drive people to your search results not some other ad sponsored search result

Since this is an article on Google Custom Search in a React/Redux app I will assume you are comfortable or somewhat familiar with React/Redux. I will also be using Express/Postman for the backend so you should be comfortable with concepts like async and await.

Overview

Before writing any code, I needed to think what exactly it is I wanted to do. I wanted to implement site wide custom search on my web app. This search input would be on the Header.js file and would be on every page of my site. When a user types into it, the search results would be stored in redux and the user would be redirected to the Google indexed search results page.

Step 1: Google Custom Search Engine API

Head over to https://developers.google.com/custom-search/v1/overview to get an custom search JSON API key from Google and create new project

[object Object]

Next head over to https://cse.google.com/all and add a search engine for your web app. Here you will need to grab the cx id, aka search engine id. This will look like a long string like this sample cx id. You will need to use your own.

[object Object]

https://developers.google.com/custom-search/v1/using_rest

Each request will require an API key, a custom search ID and query. For example

https://www.googleapis.com/customsearch/v1?key=INSERT_YOUR_API_KEY&cx=CUSTOM_SEARCH_ID&q=products

Here the query is `products`

You can try out the url in chrome or any web browser to see if it works. If you have any issues so far refer to the docs here Now let's call the google custom search API on the server side. It will be in a file called gcse.js. For me, this file is going to be in the routes/api directory where all my other routes are stored.

My gcse.js file in VS code looks like:

const express = require('express');
const request = require('request');
const router = express.Router();

const googleAPIKEY = require('../../config/keys').googleAPIKEY;
const customerSearchEngineID = require('../../config/keys').customerSearchEngineID; 

router.get('/', (req, res) => {
    try {
        res.json({msg:'GCSE works!'})
    } catch (error) {
        console.log(error)
    }
})

// Type     :   GET
// Route    :   api/google-custom-search/:query
// Desc     :   Google custom search api
// Access   :   Public, anyone can search.
router.get('/:query', (req, res) => {

    try {
        const options = {
            uri: `https://www.googleapis.com/customsearch/v1?key=${googleAPIKEY}&cx=${customerSearchEngineID}&q=${req.params.query}`,
            method: 'GET',
            headers : {'user-agent':'nodejs'}
        }
        request(options, (error, response, body)=> {
            if( error ) console.log(error);
            if(response.statusCode !== 200) return res.status(404).json({msg:'Query could not run'});
            const data = JSON.parse(body).items;

            // send back the array of items
            if(data && data.length > 0){
                res.json(data); 
            } else {
                res.status(404).send('There is no data to return');
            }
         
        })
      
    } catch (error) {
        res.status(500).send('Server Error',error);
    }
  
})


module.exports = router;

I have used a config file to hide my sensitive API keys and .gitignore to make sure I don't ever push the values to github. You can read more about that here and how to use this in a production environment using environment variables.

You can test this URL out in postman with the following sample call with react being the search query, q.

http://localhost:3000/api/gcse/react

I am using request library, but you could use axios or fetch if you prefer.

Step 2: Setting up our actions

Inside the actions folder, I have created a searchActions.js file which will be called in Header.js file when someone searches for something. This will be a fairly simple action file which can dispatch three types of options GOOGLE_SEARCH, GOOGLE_SEARCH_ERROR and CLEAR_GOOGLE_SEARCH.

The first action type will call the end point with a query from the input on the front end. It will send back a payload with the searchResults (an array) and a query (a string) of what the user searched.

The searchActions.js file:

import axios from 'axios';
import { GOOGLE_SEARCH, GOOGLE_SEARCH_ERROR, CLEAR_GOOGLE_SEARCH } from './types';

export const googleCustomSearch = query => async dispatch => {

    try {
        const res = await axios.get(`/api/gcse/${query}`);
        if(res === null || res === [] || res === ''){
            dispatch({
                type: GOOGLE_SEARCH_ERROR,
                payload: {msg: 'No Search Results'}
            })
        } else {
            dispatch({
                type: GOOGLE_SEARCH,
                payload: {
                    searchResults:res.data,
                    query:query
                }
            })
        }
    
    } catch (error) {
        console.error(error);
        dispatch({
            type: GOOGLE_SEARCH_ERROR,
            payload: {msg: 'Oops, could not find what you were searching for!'}
        })
    }
}

export const clearGoogleCustomerSearch = () => dispatch => {
    try {
        dispatch({
            type: CLEAR_GOOGLE_SEARCH
        })
    } catch (error) {
        console.error('Could not clear the search results', error)
    }
}

In case you are wondering why I have types file. This is where I store all my action types so they are stored in variables, and I only need to update in one place.

The types.js file

// GOOGLE CUSTOM SEARCH 
export const GOOGLE_SEARCH = 'GOOGLE_SEARCH';
export const GOOGLE_SEARCH_ERROR = 'GOOGLE_SEARCH_ERROR';
export const CLEAR_GOOGLE_SEARCH = 'CLEAR_GOOGLE_SEARCH';
 

STEP 3. Adding in the search reducer

Now that we have the action file set up, we need to set up the searchReducer.js file inside the reducers folder. A fairly standard set up is shown below:

import {
    GOOGLE_SEARCH,
    GOOGLE_SEARCH_ERROR,
    CLEAR_GOOGLE_SEARCH
} from '../actions/types';

const initialState = {
    searchResults: [],
    query: '',
    loading: true,
    error: {}
}

export default function( state = initialState, action ){ 
    const { type, payload } = action;
    switch( type ){
       case GOOGLE_SEARCH:
           return {
               ...state,
               searchResults: payload.searchResults,
               loading: false,
               query: payload.query,
               error: {}
           } 
        case GOOGLE_SEARCH_ERROR:
            return {
                ...state,
                error: payload,
                loading: false
            }
        case CLEAR_GOOGLE_SEARCH:
            return {
                ...state,
                searchResults: [],
                query: '',
                loading: true,
                error: {}
            }
        default:
            return state
    }
}

Step 4: Calling the Action in our React App

We can finally call the googleCustomSearch and clearGoogleCustomSearch actions in our Header.js file

Will have a form component which will call siteSearchHandler. This function will than call the two actions above whenever our form is submitted. In this case the form is a simple input, but I decided to wrap it in a form tag so that siteSearchHandler gets called when the enter key is pressed.

The form component:

    <form 
        onSubmit = {siteSearchHandler.bind(this)}>
    <div>
        <input
            type="text" 
            id="site-search-google" 
            name="query" 
            title="Search Site" 
            alt="Search Text" maxLength="256" 
            placeholder = "Search Site" 
            value = { query }
            onChange={e => this.onChange(e)}
        />
    </div>
    </form>

siteSearchHandler:

    const siteSearchHandler = e => {
        e.preventDefault();

        clearGoogleCustomSearch();
        googleCustomSearch(this.state.query);
        history.push('/search');
    } 

The Header.js file is your typical functional based component ( shortcut racfp + tab in VS code with the ReactJs code snippets extension )

import React, {useState} from 'react'
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import PropTypes from 'prop-types';
import { googleCustomSearch, clearGoogleCustomSearch } from '../../actions/searchActions';

const Header = ({googleCustomSearch, clearGoogleCustomSearch, history}) => {

    const [query, setQuery] = useState('');

    const siteSearchHandler = e => {
        e.preventDefault();
        clearGoogleCustomSearch();
        googleCustomSearch(query);
        history.push('/search');
    }
    const onChangeHandler = e => {
        setQuery(e.target.value);
    }
    return (
        <div> 
            <form 
                onSubmit = {siteSearchHandler.bind(this)}>
                <div>
                    <input
                        type="text" 
                        id="site-search-google" 
                        name="query" 
                        title="Search Site" 
                        alt="Search Text" maxLength="50" 
                        placeholder = "Search Site" 
                        value = { query }
                        onChange={e => onChangeHandler(e)}
                    />
                </div>
            </form> 
        </div>
    )
}

Header.propTypes = {
    search: PropTypes.object.isRequired
}

const mapStateToProps = state => ({
    search: state.search
})

export default connect(mapStateToProps, { googleCustomSearch, clearGoogleCustomSearch })(withRouter(Header))

Everything is based as props into the functional based Header component, including the history, which we are getting by wrapping our Header component with the hoc (higher order component) withRouter. This allows as to move to a new route, '/search' where all our search results are displayed.

Step 5: Displaying the search results.

This should be quite straight forward now. All we need to do is grab our search results from the redux state and map over it, to show the different routes a user can go to in our site from the search results. One thing I noticed was that Google was indexing some http results from my site. To fix that quickly I simply used a regular expression.

const link = url.replace(/^http:\/\//i, 'https://').replace("https://www.cravejs.com/","/");
The final searchResults.js:

import React, { Fragment } from 'react'
import { connect } from 'react-redux';
import { withRouter } from 'react-router-dom';
import LoadingSpinner from '../utils/LoadingSpinner';

const SearchResults = ({search: {loading, searchResults, error, query}, history}) => {
    
    const goToLink = url => {
        const link = url.replace(/^http:\/\//i, 'https://').replace("https://www.cravejs.com/","/");
       
        history.push(link);
    }
    if ( loading ) return   <div className = "SearchResults__summary"> <LoadingSpinner/> </div>; 

    if(searchResults){

        return (
                error && error.msg ? 
                <div className = "SearchResults__summary"> Oops could not find what you were looking for </div> :

                <Fragment>
                    
                    { 
                        searchResults.length > 0 ? 
                            <div className = "SearchResults__summary">
                            There are { searchResults.length } results for <strong> {query} </strong> 
                            </div> : 
                            <div className = "SearchResults__summary">Please start searching at the top</div>
                    }
                  
                <div className = "SearchResults">  
                    <div className = "SearchResults__list">
                    { searchResults && searchResults.map( (result,i) => (
                        <div onClick = { goToLink.bind(this,result.link) } className = "SearchResults__link" to = {result.link} key = {`${i} - ${result.title}`} > 
                            <h4> { result.title } </h4>
                            <p> {result.snippet}  </p>
                        </div>
                    )) }
                    </div>
                </div>
                <div className = "SearchResults__message"> Still haven't find what you are looking for? Try the explore option above </div>
                </Fragment>
               
        )      
    }
}

const mapStateToProps = state => ({
    search: state.search
})

export default connect( mapStateToProps )(withRouter(SearchResults))
 

Summary

This article quickly illustrates how to use React/Redux with Google Custom Search. The steps involved are first getting the API to work and testing the request and response with a tool like Postman.

Next an action file is created to call this API endpoint. This action dispatches a type and payload which triggers the reducer to change the state of the app. In React, we can then use that state in various components by using connecting React component with Redux.

Final Thoughts

In order for Google Custom Search to work better, we should submit a site map and make our app SEO friendly. This will make our site wide Google Custom search work better and will also enable us to appear in Google search results. Win win right ?! I will be writing an article on this soon.