Tags

By year

  1. 2018 (2)
  2. 2017 (4)
  3. 2015 (9)

Making a Simple Image Viewer using React and Express

Wikipedia thumbnail image viewer React Redux react-router Bootstrap Express Node JavaScript

Posted on Aug 13


A simple app written as my own hands-on exercise to get a quick grasp of React, Redux and react-router following some tutorial sites:

The Repo

The sources can be retrieved from here.

The App

The app has a navbar on top with a text box. Wikipedia articles can be retrieved by typing in a keyword.

Top navbar

Upon completion of search, matching Wikipedia articles are listed in the center of the page. Some of the articles come with a corresponding thumbnail image.

List of Matching Articles

By clicking one of the thumbnails, a larger thumbnail is displayed in a modal window. This window can be closed by clicking anywhere on the page.

Modal Image View

The Architecture

  1. The client pushes the path /search with a keyword parameter to the history stack.
  2. The client mounts the Gallery component for rendering search results. Upon mounting of this component, it calls the server API asynchronously and dispatches the START_SEARCHING_FOR_KEYWORD action.
  3. The server fetches Wikipedia article titles and thumbnail URLs via Mediawiki API.
  4. The client dispatches the RECEIVED_THUMBNAILS action upon a successful response from the server.
  5. The client renders the search results.
  6. The client renders a modal window to view a larger thumbnail when the user clicks on a smaller thumbanil.

Architecture

The Server

Defining the API

The server uses Express. It has only one route /api/search which is defined as below.

// Excerpt from ./server/app.js
app.get('/api/search', (req, res) => {
  wikipedia.fetchTitlesAndThumbnails(req.query.kw) // Defined in ./server/wikipedia.js
    .then(result => {
      res.json(result)
    })
    .catch(error => {
      console.error(error.toString());
      res.status(500).json({error: error.toString()});
    });
});

The Mediawiki API

The server uses two Mediawiki APIs; opensearch and query(pageimages). The file ./server/wikipedia.js wraps calls to these external APIs.

Example Mediawiki Queries

The Client

The Search Input Box

The TopNavbar React component has the history prop passed by BrowserRouter. In the submit event handler, the successive path /search is pushed to the history.

The Routes

The home route / in this app navigates to the Home component that is just a placeholder containing only a link to an example thumbnail query.

The path /search navigates to the Gallery component for displaying a list of articles and thumbnails retrieved by the server.

        <Switch location={isModal ? this.previousLocation : location}>
          <Route exact path='/' component={Home}/>
          <Route path='/search' component={Gallery} />
        </Switch>

The implementation of modal window apparently mimics the "Modal Gallery" example of React Training / React Router.

Below is an excerpt from the client source. The location.state object is defined when a smaller thumbnail is clicked. If the isModal flag is set, the Switch route is rendered according to the previous location (this.previousLocation). And the Modal component is placed on top of the previous rendering.

  render() {
    const { location } = this.props
    const isModal = !!(
      location.state &&
      location.state.modal &&
      this.previousLocation !== location // not initial render
    )
    return (
      <div>
        <Switch location={isModal ? this.previousLocation : location}>
          <Route exact path='/' component={Home}/>
          <Route path='/search' component={Gallery} />
        </Switch>
        {
          isModal
          ? <Route path='/img' component={Modal} />
          : null
        }
      </div>
    )
  }

The API Invocation and the State Transition

The Gallery component calls its fetchThumbnails method when it is mounted or its props are to be updated.

class Gallery extends React.Component {
  fetchThumbnails(params) {
    const dispatch = this.props.dispatch;
    const loading = this.props.loading;
    const key = params.keyword;
    console.log('fetchThumbnails', key);
    if (loading) {
      return null;
    }
    dispatch({type: 'START_SEARCHING_FOR_KEYWORD', keyword: key});
    return axios.get('/api/search', {params: {kw: key}})
      .then(response => {
        let result = response.data;
        console.log("fetchThumbnails result", result);
        dispatch({
          type: 'RECEIVED_THUMBNAILS',
          keyword: key,
          thumbnails: result
        });
      })
      .catch(error => {
        console.log("Error in fetchThumbnails", error);
        dispatch({
          type: 'ERROR_IN_API',
          keyword: key,
          error: error
        });
      });
  }
  componentDidMount() {
    let query = queryString.parse(this.props.location.search);
    this.fetchThumbnails(query);
  }
  ...
}

This method invokes the server API. The API is called using axios.get. It dispatches the following Redux actions during the asynchronous run.

  • Start of search:

    {
      type: 'START_SEARCHING_FOR_KEYWORD',
      keyword: key /* keyword from user */
    }
    
  • Completion of search:

    {
      type: 'RECEIVED_THUMBNAILS',
      keyword: key, /* keyword from user */
      thumbnails: result /* result of the API */
    }
    
  • Error:

    {
      type: 'ERROR_IN_API',
      keyword: key, /* keyword from user */
      error: error /* the error caught during axios.get */
    }
    

The Reducer for Redux Store

const INITIAL_STATE = {
  loading: false,
  keyword: '',
  thumbnails: {},
  error: false
}

export default (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case 'START_SEARCHING_FOR_KEYWORD':
      return {
        loading: true,
        keyword: action.keyword,
        thumbnails: {},
        error: false
      }
    case 'RECEIVED_THUMBNAILS':
      return {
        loading: false,
        keyword: action.keyword,
        thumbnails: action.thumbnails,
        error: false
      }
    case 'ERROR_IN_API':
      return {
        loading: false,
        keyword: action.keyword,
        thumbnails: {},
        error: action.error
      }
    default:
      return state
  }
}

Testing Both the Webpack Dev Server of create-react-app and Express API Server

The client is made with create-react-app so the Webpack Dev Server is used when testing on the development version of the app. Getting this server to work with the Express API server requires additional configurations below.

This article helped me a lot:

The following command is designated in package.json as one of scripts to start the two servers concurrently.

  "start": "concurrently \"node server\" \"react-scripts start\""

The actual command by yarn run start:

concurrently "node server" "react-scripts start"

The Webpack dev server call the Express API via the proxy defined in the package.json like below:

  "proxy": {
    "/api": {
      "target": "http://localhost:7070"
    }
  },

If we are running the servers on a remote host (such as EC2 instance) instead of localhost, its host name must be specified to the HOST variable in the .env.development file:

HOST=ec2-xxx.compute.amazonaws.com

References


2015 My gh-pages