DEV Community

Cover image for Build an amazing Job Search App using React
Yogesh Chavan
Yogesh Chavan

Posted on • Updated on

Build an amazing Job Search App using React

In this article, you will build a beautiful Job search app using Github Jobs API

By building this App, you will learn:

  1. How to lazy load images in React
  2. How to use React Context API for sharing data between components
  3. Why React will not render the HTML used in JSX Expression and how to get it displayed correctly when required
  4. How to display an alternate loading image while the actual image is downloading
  5. How to create your own version of a loader using React Portal
  6. How to add Load More functionality

And much more.

You can see the live demo of the application HERE

Let’s get started

Initial Setup

Create a new project using create-react-app

create-react-app github-jobs-react-app
Enter fullscreen mode Exit fullscreen mode

Once the project is created, delete all files from the src folder and create index.js file inside the src folder. Also create actions,components, context,css, custom-hooks, images,reducers, router, store and utils folders inside the src folder.

Install the necessary dependencies

yarn add axios@0.19.2 bootstrap@4.5.0 lodash@4.17.15 moment@2.27.0 node-sass@4.14.1 prop-types@15.7.2 react-bootstrap@1.0.1 react-redux@7.2.0 redux@4.0.5 redux-thunk@2.3.0
Enter fullscreen mode Exit fullscreen mode

Create a new folder with the name server outside the src folder and execute the following command from server folder

yarn init -y
Enter fullscreen mode Exit fullscreen mode

This will create a package.json file inside the server folder.

Install the required dependencies from server folder

yarn add axios@0.19.2 express@4.17.1 cors@2.8.5 nodemon@2.0.4
Enter fullscreen mode Exit fullscreen mode

Create a new file with name .gitignore inside server folder and add the following line inside it so node_modules folder will not be version controlled

node_modules
Enter fullscreen mode Exit fullscreen mode

Initial Page Display Changes

Now, Create a new file styles.scss inside src/css folder and add content from HERE inside it.

Create a new file jobs.js inside src/reducers folder with the following content

const jobsReducer = (state = [], action) => {
  switch (action.type) {
    case 'SET_JOBS':
      return action.jobs;
    case 'LOAD_MORE_JOBS':
      return [...state, ...action.jobs];
    default:
      return state;
  }
};
export default jobsReducer;
Enter fullscreen mode Exit fullscreen mode

In this file, we are adding the new jobs data coming from API in redux using SET_JOBS action and using LOAD_MORE_JOBS action we are getting more jobs and adding it to already existing jobs array using the spread operator.

[...state, ...action.jobs]
Enter fullscreen mode Exit fullscreen mode

Create a new file errors.js inside src/reducers folder with the following content

const errorsReducer = (state = {}, action) => {
  switch (action.type) {
    case 'SET_ERRORS':
      return {
        error: action.error
      };
    case 'RESET_ERRORS':
      return {};
    default:
      return state;
  }
};
export default errorsReducer;
Enter fullscreen mode Exit fullscreen mode

In this file, we are adding the API error if any into the redux store by dispatching SET_ERRORS action and removing the error object from redux store if there is no error while getting a response from API by dispatching RESET_ERRORS action.

Create a new file store.js inside src folder with the following content

import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import jobsReducer from '../reducers/jobs';
import errorsReducer from '../reducers/errors';

const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;

const store = createStore(
  combineReducers({
    jobs: jobsReducer,
    errors: errorsReducer
  }),
  composeEnhancers(applyMiddleware(thunk))
);

console.log(store.getState());

export default store;
Enter fullscreen mode Exit fullscreen mode

In this file, we are creating a redux store that uses combineReducers and added thunk from redux-thunk as a middleware for managing the Asynchronous API handling.

We also added the redux devtool configuration using composeEnhandlers.
If you are new to redux-thunk and redux devtool configuration, check out my previous article HERE to understand how to use it.

Now, inside src/index.js file add the following content

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import store from './store/store';
import HomePage from './components/HomePage';
import 'bootstrap/dist/css/bootstrap.min.css';
import './css/styles.scss';

ReactDOM.render(
  <Provider store={store}>
    <HomePage />
  </Provider>,
  document.getElementById('root')
);
Enter fullscreen mode Exit fullscreen mode

In this file, we are using Provider component from react-redux which will allow us to share the store data to HomePage and all its child components.

Now, Create a new file HomePage.js inside src/components folder with the following content.

import React from 'react';

const HomePage = () => {
  return <div className="container">Home Page</div>;
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

Now, open public/index.html and change

<title>React App</title>
Enter fullscreen mode Exit fullscreen mode

To

<title>Github Job Search</title>
Enter fullscreen mode Exit fullscreen mode

Now start the React application by running following command from github-jobs-react-app folder

yarn start
Enter fullscreen mode Exit fullscreen mode

You will see the application with Home Page text displayed

initial screen

Adding Basic Search UI

Now, create a new file Header.js inside components folder with the following content

import React from 'react';

const Header = () => (
  <header className="header">
    <div className="title">Github Job Search</div>
  </header>
);

export default Header;
Enter fullscreen mode Exit fullscreen mode

Create a new file Search.js inside components folder with the following content

import React, { useState } from 'react';
import { Form, Button, Row, Col } from 'react-bootstrap';

const Search = (props) => {
  const [state, setState] = useState({
    description: '',
    location: '',
    full_time: false
  });

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    if (name === 'full_time') {
      setState((prevState) => ({ ...state, [name]: !prevState.full_time }));
    } else {
      setState({ ...state, [name]: value });
    }
  };

  const handleSearch = (event) => {
    event.preventDefault();
    console.log(state);
  };

  return (
    <div className="search-section">
      <Form className="search-form" onSubmit={handleSearch}>
        <Row>
          <Col>
            <Form.Group controlId="description">
              <Form.Control
                type="text"
                name="description"
                value={state.description || ''}
                placeholder="Enter search term"
                onChange={handleInputChange}
              />
            </Form.Group>
          </Col>
          <Col>
            <Form.Group controlId="location">
              <Form.Control
                type="text"
                name="location"
                value={state.location || ''}
                placeholder="Enter location"
                onChange={handleInputChange}
              />
            </Form.Group>
          </Col>
          <Col>
            <Button variant="primary" type="submit" className="btn-search">
              Search
            </Button>
          </Col>
        </Row>
        <div className="filters">
          <Form.Group controlId="full_time">
            <Form.Check
              type="checkbox"
              name="full_time"
              className="full-time-checkbox"
              label="Full time only"
              checked={state.full_time}
              onChange={handleInputChange}
            />
          </Form.Group>
        </div>
      </Form>
    </div>
  );
};
export default Search;
Enter fullscreen mode Exit fullscreen mode

In this file, we have added two input text fields to get the description and location from the user and added a checkbox to get only full-time jobs.

We also added an onChange handler to each input field to update the state value.

Now, open HomePage.js and replace it with the following content

import React from 'react';
import Header from './Header';
import Search from './Search';

const HomePage = () => {
  return (
    <div>
      <Header />
      <Search />
    </div>
  );
};

export default HomePage;
Enter fullscreen mode Exit fullscreen mode

Now, If you enter the values in input fields and click on Search button, you will see the entered data displayed in the console

search page

Displaying List of Jobs on UI

Now, create errors.js inside src/actions folder with the following content

export const setErrors = (error) => ({
  type: 'SET_ERRORS',
  error
});

export const resetErrors = () => ({
  type: 'RESET_ERRORS'
});
Enter fullscreen mode Exit fullscreen mode

In this file, we have added action creator functions which we will call to dispatch actions to the reducer.

Create a new file constants.js inside utils folder with the following content

export const BASE_API_URL = 'http://localhost:5000';
Enter fullscreen mode Exit fullscreen mode

Create a new file jobs.js inside src/actions folder with the following content

import axios from 'axios';
import moment from 'moment';
import { BASE_API_URL } from '../utils/constants';
import { setErrors } from './errors';

export const initiateGetJobs = (data) => {
  return async (dispatch) => {
    try {
      let { description, full_time, location, page } = data;
      description = description ? encodeURIComponent(description) : '';
      location = location ? encodeURIComponent(location) : '';
      full_time = full_time ? '&full_time=true' : '';

      if (page) {
        page = parseInt(page);
        page = isNaN(page) ? '' : `&page=${page}`;
      }

      const jobs = await axios.get(
        `${BASE_API_URL}/jobs?description=${description}&location=${location}${full_time}${page}`
      );
      const sortedJobs = jobs.data.sort(
        (a, b) =>
          moment(new Date(b.created_at)) - moment(new Date(a.created_at))
      );
      return dispatch(setJobs(sortedJobs));
    } catch (error) {
      error.response && dispatch(setErrors(error.response.data));
    }
  };
};

export const setJobs = (jobs) => ({
  type: 'SET_JOBS',
  jobs
});

export const setLoadMoreJobs = (jobs) => ({
  type: 'LOAD_MORE_JOBS',
  jobs
});
Enter fullscreen mode Exit fullscreen mode

In this file, we have added an initiateGetJobs function which will get the JSON data by making an API call to the Express server in Node.js and once the data is received, SET_JOBS action is dispatched which will add all the jobs data into the redux store by executing the SET_JOBS switch case from reducers/jobs.js file.

Now, create a new fileserver.js inside server folder with the following content

const path = require('path');
const axios = require('axios');
const cors = require('cors');
const express = require('express');
const app = express();
const PORT = process.env.PORT || 5000;
const buildPath = path.join(__dirname, '..', 'build');

app.use(express.static(buildPath));
app.use(cors());

app.get('/jobs', async (req, res) => {
  try {
    let { description = '', full_time, location = '', page = 1 } = req.query;
    description = description ? encodeURIComponent(description) : '';
    location = location ? encodeURIComponent(location) : '';
    full_time = full_time === 'true' ? '&full_time=true' : '';

    if (page) {
      page = parseInt(page);
      page = isNaN(page) ? '' : `&page=${page}`;
    }

    const query = `https://jobs.github.com/positions.json?description=${description}&location=${location}${full_time}${page}`;
    const result = await axios.get(query);
    res.send(result.data);
  } catch (error) {
    res.status(400).send('Error while getting list of jobs.Try again later.');
  }
});

app.listen(PORT, () => {
  console.log(`server started on port ${PORT}`);
});
Enter fullscreen mode Exit fullscreen mode

In this file, we have created a /jobs get API using Express server.

Here, we are calling the Github Jobs API to get the list of available jobs by passing the description and location.

By default, the API gives a list of the latest 50 jobs only but we can get more jobs by sending page query parameter with values 1, 2, 3, etc.

So we are validating the page query parameter by the following code

if (page) {
  page = parseInt(page);
  page = isNaN(page) ? '' : `&page=${page}`;
}
Enter fullscreen mode Exit fullscreen mode

If we want to only the full-time jobs then we need to add an additional full_time parameter to query string with the value of true

full_time = full_time === 'true' ? '&full_time=true' : '';
Enter fullscreen mode Exit fullscreen mode

Then finally we are creating the API URL by combining all parameter values.

`https://jobs.github.com/positions.json?description=${description}&location=${location}${full_time}${page}`;
Enter fullscreen mode Exit fullscreen mode

The reason for adding encodeURIComponent for each input field is to convert special characters if any like space to %20.

If you noticed, we have added the same parsing code in initiateGetJobs function also which is inside actions/jobs.js file.

The reason for including it in server code also is that we can also directly access the /jobs get API without any application for just for the additional check we added the conditions.

Now, create a new file JobItem.js inside the components folder with the following content

import React from 'react';
import moment from 'moment';

const JobItem = (props) => {
  const {
    id,
    type,
    created_at,
    company,
    location,
    title,
    company_logo,
    index
  } = props;

  return (
    <div className="job-item" index={index + 1}>
      <div className="company-logo">
        <img src={company_logo} alt={company} width="100" height="100" />
      </div>
      <div className="job-info">
        <div className="job-title">{title}</div>
        <div className="job-location">
          {location} | {type}
        </div>
        <div className="company-name">{company}</div>
      </div>
      <div className="post-info">
        <div className="post-time">
          Posted {moment(new Date(created_at)).fromNow()}
        </div>
      </div>
    </div>
  );
};

export default JobItem;
Enter fullscreen mode Exit fullscreen mode

In this file, we are displaying the data coming from API
Create a new file Results.js inside components folder with the following content

import React from 'react';
import JobItem from './JobItem';

const Results = ({ results }) => {
  return (
    <div className="search-results">
      {results.map((job, index) => (
        <JobItem key={job.id} {...job} index={index} />
      ))}
    </div>
  );
};

export default Results;
Enter fullscreen mode Exit fullscreen mode

In this file, we are looping through each job object from results array and we are passing the individual job data to display in JobItem component created previously.

Now, open components/HomePage.js file and replace it with the following content

import React, { useState, useEffect } from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { initiateGetJobs } from '../actions/jobs';
import { resetErrors } from '../actions/errors';
import Header from './Header';
import Search from './Search';
import Results from './Results';

const HomePage = (props) => {
  const [results, setResults] = useState([]);
  const [errors, setErrors] = useState(null);
  const [isLoading, setIsLoading] = useState(false);

  useEffect(() => {
    setResults(props.jobs);
  }, [props.jobs]);

  useEffect(() => {
    setErrors(props.errors);
  }, [props.errors]);

  const loadJobs = (selection) => {
    const { dispatch } = props;
    const { description, location, full_time, page = 1 } = selection;
    dispatch(resetErrors());
    setIsLoading(true);
    dispatch(initiateGetJobs({ description, location, full_time, page }))
      .then(() => {
        setIsLoading(false);
      })
      .catch(() => setIsLoading(false));
  };

  const handleSearch = (selection) => {
    loadJobs(selection);
  };

  return (
    <div>
      <Header />
      <Search onSearch={handleSearch} />
      {!_.isEmpty(errors) && (
        <div className="errorMsg">
          <p>{errors.error}</p>
        </div>
      )}
      <Results results={results} />
      {isLoading && <p className="loading">Loading...</p>}
    </div>
  );
};

const mapStateToProps = (state) => ({
  jobs: state.jobs,
  errors: state.errors
});

export default connect(mapStateToProps)(HomePage);
Enter fullscreen mode Exit fullscreen mode

In this file, we are starting to use React Hooks now. If you are new to React Hooks check out my previous article for an introduction to Hooks HERE

Let’s understand the code from the HomePage component.
Initially, we declared state variables using useState hook to store the result from API in an array and a flag for showing the loading and object for an error indication.

const [results, setResults] = useState([]);
const [errors, setErrors] = useState(null);
const [isLoading, setIsLoading] = useState(false);
Enter fullscreen mode Exit fullscreen mode

Then we call the useEffect Hook to get the list of jobs and error if any

useEffect(() => {
  setResults(props.jobs);
}, [props.jobs]);
useEffect(() => {
  setErrors(props.errors);
}, [props.errors]);
Enter fullscreen mode Exit fullscreen mode

We implement the componentDidUpdate lifecycle method of class components using the useEffect hook by passing the dependency array as the second argument. So each of these useEffect hooks will be executed only when their dependency changes For example when props.jobs changes or props.errors changes. The data is available in props because we have added a mapStateToProps method at the end of the file

const mapStateToProps = (state) => ({
  jobs: state.jobs,
  errors: state.errors
});
Enter fullscreen mode Exit fullscreen mode

and passed it to connect the method of react-redux library.

export default connect(mapStateToProps)(HomePage);
Enter fullscreen mode Exit fullscreen mode

Then, we are passing the onSearch prop to the Search component whose value is the handleSearch function.

<Search onSearch={handleSearch} />
Enter fullscreen mode Exit fullscreen mode

From inside this function, we are calling the loadJobs function which is calling the initiateGetJobs action creator function to make an API call to the Express server.

We are passing the onSearch prop to the Search component, but we are not using it yet, so let’s use it first.

Open Search.js component and change

const handleSearch = (event) => {
  event.preventDefault();
  console.log(state);
};
Enter fullscreen mode Exit fullscreen mode

to

const handleSearch = (event) => {
  event.preventDefault();
  console.log(state);
  props.onSearch(state);
};
Enter fullscreen mode Exit fullscreen mode

So now, when we click the Search button, we are calling onSearch function passed as a prop to the Search component from the HomePage component.

Now, let’s run the application. Before running it, we need to make some changes.

Open server/package.json file and add start script inside it

"start": "nodemon server.js"
Enter fullscreen mode Exit fullscreen mode

So the package.json from server folder will look like this

{
  "name": "server",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "nodemon server.js"
  },
  "dependencies": {
    "axios": "0.19.2",
    "cors": "2.8.5",
    "express": "4.17.1",
    "nodemon": "^2.0.4",
  }
}
Enter fullscreen mode Exit fullscreen mode

Now, execute start command from server folder

yarn run start
Enter fullscreen mode Exit fullscreen mode

This will start the Express server.

Open another terminal and from github-jobs-react-app folder, execute yarn run start command. This will start your React application.

The description and location are optional parameters to the Github Jobs API so If you don’t enter any value and click on the Search button, you will get all the available jobs displayed on the screen sorted by the posted date

results page

working search

The data is sorted by creation date in initiateGetJobs function inside the actions/jobs.js file

const sortedJobs = jobs.data.sort(
  (a, b) =>
    moment(new Date(b.created_at)) - moment(new Date(a.created_at))
);
Enter fullscreen mode Exit fullscreen mode

If you want to dive into details of how this code sorted the data, check out my previous article HERE

You can find source code until this point HERE

Displaying Job Details Page

Now, let’s get the details of the Job when we click on any of the Job from the

Create a new file JobDetails.js inside components folder with the following content

import React from 'react';

const JobDetails = ({ details, onResetPage }) => {
  const {
    type,
    title,
    description,
    location,
    company,
    company_url,
    company_logo,
    how_to_apply
  } = details;

  return (
    <div className="job-details">
      <div className="back-link">
        <a href="/#" onClick={onResetPage}>
          &lt;&lt; Back to results
        </a>
      </div>
      <div>
        {type} / {location}
      </div>
      <div className="main-section">
        <div className="left-section">
          <div className="title">{title}</div>
          <hr />
          <div className="job-description">{description}</div>
        </div>
        <div className="right-section">
          <div className="company-details">
            <h3>About company</h3>
            <img src={company_logo} alt={company} className="company-logo" />
            <div className="company-name">{company}</div>
            <a className="company-url" href={company_url}>
              {company_url}
            </a>
          </div>
          <div className="how-to-apply">
            <h3>How to apply</h3>
            <div>{how_to_apply}</div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default JobDetails;
Enter fullscreen mode Exit fullscreen mode

Here, we are displaying the description of the job details.

Now, we need a flag that will decide when to display the details page and when to display the list of jobs.

So create a new state variable inside HomePage.js file with a default value of home and a variable to track id of the job clicked

const [jobId, setJobId] = useState(-1);
const [page, setPage] = useState('home');
Enter fullscreen mode Exit fullscreen mode

Open HomePage.js file and replace it with the following content

import React, { useState, useEffect } from 'react';
import _ from 'lodash';
import { connect } from 'react-redux';
import { initiateGetJobs } from '../actions/jobs';
import { resetErrors } from '../actions/errors';
import Header from './Header';
import Search from './Search';
import Results from './Results';
import JobDetails from './JobDetails';

const HomePage = (props) => {
  const [results, setResults] = useState([]);
  const [errors, setErrors] = useState(null);
  const [isLoading, setIsLoading] = useState(false);
  const [jobId, setJobId] = useState(-1);
  const [page, setPage] = useState('home');

  useEffect(() => {
    setResults(props.jobs);
  }, [props.jobs]);

  useEffect(() => {
    setErrors(props.errors);
  }, [props.errors]);

  const loadJobs = (selection) => {
    const { dispatch } = props;
    const { description, location, full_time, page = 1 } = selection;
    dispatch(resetErrors());
    setIsLoading(true);
    dispatch(initiateGetJobs({ description, location, full_time, page }))
      .then(() => {
        setIsLoading(false);
      })
      .catch(() => setIsLoading(false));
  };

  const handleSearch = (selection) => {
    loadJobs(selection);
  };

  const handleItemClick = (jobId) => {
    setPage('details');
    setJobId(jobId);
  };

  const handleResetPage = () => {
    setPage('home');
  };

  let jobDetails = {};
  if (page === 'details') {
    jobDetails = results.find((job) => job.id === jobId);
  }
  return (
    <div>
      <div className={`${page === 'details' && 'hide'}`}>
        <Header /> <Search onSearch={handleSearch} />
        {!_.isEmpty(errors) && (
          <div className="errorMsg">
            <p>{errors.error}</p>
          </div>
        )}
        {isLoading && <p className="loading">Loading...</p>}
        <div>
          <Results results={results} onItemClick={handleItemClick} />
        </div>
      </div>
      <div className={`${page === 'home' && 'hide'}`}>
        <JobDetails details={jobDetails} onResetPage={handleResetPage} />
      </div>
    </div>
  );
};

const mapStateToProps = (state) => ({
  jobs: state.jobs,
  errors: state.errors
});

export default connect(mapStateToProps)(HomePage);
Enter fullscreen mode Exit fullscreen mode

In this file, we have added handleItemClick and handleResetPage functions.
Also when we click on the details page, we filter out the job from the results array

let jobDetails = {};
if (page === 'details') {
  jobDetails = results.find((job) => job.id === jobId);
}
Enter fullscreen mode Exit fullscreen mode

and pass it to JobDetails component

<JobDetails details={jobDetails} onResetPage={handleResetPage} />
Enter fullscreen mode Exit fullscreen mode

If the page value is home, we are displaying the Header, Search and the Results components and if the value is details, we are displaying the JobDetails page as we are adding the hide CSS class to display respective components

Note, we also passed onItemClick prop to Results component.

<Results results={results} onItemClick={handleItemClick} />
Enter fullscreen mode Exit fullscreen mode

and from Results component, we are passing it down to JobItem component and inside that component we have added that handler to the topmost div

<div className="job-item" index={index + 1} onClick={() => onItemClick(id)}>
Enter fullscreen mode Exit fullscreen mode

where we are destructuring the id from props and passing it to onItemClick function

Now, restart your React application and Express server by running yarn run start command and verify the changes

details page

So now, when we click on any job, we can see the details of the job but if you noticed the details page, you can see that the HTML of the details page is displayed as it is which means the

tag is displayed as static text instead of rendering the paragraph.

This because by default React does not directly display the HTML content when used inside the JSX Expression to avoid the Cross Site Scripting (XSS) attacks. React escapes all the html content provided in the JSX Expression which is written in curly brackets so it will be printed as it is.

api

If you check the above API response, you can see that the description field contains the HTML content and we are printing the description in JobDetails.js file as

<div className="job-description">{description}</div>
Enter fullscreen mode Exit fullscreen mode

Also, in the how to apply section

<div>{how_to_apply}</div>
Enter fullscreen mode Exit fullscreen mode

To display the HTML content if its the requirement as in our case, we need to use a special prop called dangerouslySetInnerHTML and pass it the HTML in the __html field as shown below

<div className="job-description" dangerouslySetInnerHTML={{ __html: description }}></div>
Enter fullscreen mode Exit fullscreen mode

and

<div dangerouslySetInnerHTML={{ __html: how_to_apply }}></div>
Enter fullscreen mode Exit fullscreen mode

So make these changes in JobDetails.js file and check the application now, You will see the HTML rendered correctly

correct html

Awesome!

Just one more thing, while building application, it's not good to keep sending requests to the actual server every time we are testing so create a new file jobs.json inside public folder by saving the response of API from HERE and in actions/jobs.js file add a comment for the following line

const jobs = await axios.get(
  `${BASE_API_URL}/jobs?description=${description}&location=${location}${full_time}${page}`
);
Enter fullscreen mode Exit fullscreen mode

and add the following code below it.

const jobs = await axios.get('./jobs.json');
Enter fullscreen mode Exit fullscreen mode

job action

So now, whenever we click on the Search button, we will take data from the JSON file stored in public folder which will give a faster response and will also not increase the number of requests to the actual Github API.

If you are using some other APIs, they might be limited to a specific number of requests and might charge you if you exceed the limit.
Note: Github Jobs API is free and will not charge you for the number of requests but still it’s good to use a cached response and only when you need to handle proper scenarios, use the actual API instead of cached one.

You can find code until this point HERE

Using Context API to Avoid Prop Drilling

Now, if you check the HomePage component, we are passing the onItemClick function to Results component and Results component passes it down to JobItem component without using it So to avoid this prop drilling and to make the JSX returned from HomePage component a lot simpler we can use React Context API here.

If you are not familiar with React Context API, check out my previous article HERE

Inside src/context folder, create a new file jobs.js with the following content

import React from 'react';

const JobsContext = React.createContext();

export default JobsContext;
Enter fullscreen mode Exit fullscreen mode

Here, we are just creating a Context which we can use to access data in other components
In the HomePage.js file, import this context at the top of the file

import JobsContext from '../context/jobs';
Enter fullscreen mode Exit fullscreen mode

and just before returning the JSX, create a value object with the data we want to access in other components

const value = {
  results,
  details: jobDetails,
  onSearch: handleSearch,
  onItemClick: handleItemClick,
  onResetPage: handleResetPage
};
Enter fullscreen mode Exit fullscreen mode

Change the returned JSX from

return (
  <div>
    <div className={`${page === 'details' && 'hide'}`}>
      <Header />
      <Search onSearch={handleSearch} />
      {!_.isEmpty(errors) && (
        <div className="errorMsg">
          <p>{errors.error}</p>
        </div>
      )}
      {isLoading && <p className="loading">Loading...</p>}
      <Results results={results} onItemClick={handleItemClick} />
    </div>
    <div className={`${page === 'home' && 'hide'}`}>
      <JobDetails details={jobDetails} onResetPage={handleResetPage} />
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

to

return (
  <JobsContext.Provider value={value}>
    <div className={`${page === 'details' && 'hide'}`}>
      <Header /> 
      <Search />
      {!_.isEmpty(errors) && (
        <div className="errorMsg">
          <p>{errors.error}</p>
        </div>
      )}
      {isLoading && <p className="loading">Loading...</p>} 
      <Results />
    </div>
    <div className={`${page === 'home' && 'hide'}`}>
      <JobDetails />
    </div>
  </JobsContext.Provider>
);
Enter fullscreen mode Exit fullscreen mode

As you can see, we have removed all the props passed to Search, Results and JobDetails component and we are using

<JobsContext.Provider value={value}>
Enter fullscreen mode Exit fullscreen mode

to pass all of those values because Provider component requires a value prop and now all the components in between the opening and closing JobsContext.Provider tag can access any value from the value object passed as prop.

Now, open Search.js file and add the import for the context at the top. Also import the useContext hook at the top

import React, { useState, useContext } from 'react';
Enter fullscreen mode Exit fullscreen mode

Now, to access the data from value object add the following code inside Search component

const { onSearch } = useContext(JobsContext);
Enter fullscreen mode Exit fullscreen mode

Now, you can remove the props parameter passed to the component and inside handleSearch function, change

props.onSearch(state);
Enter fullscreen mode Exit fullscreen mode

to just

onSearch(state);
Enter fullscreen mode Exit fullscreen mode

Now, your Search component will look like this

import React, { useState, useContext } from 'react';
import { Form, Button, Row, Col } from 'react-bootstrap';
import JobsContext from '../context/jobs';

const Search = () => {
  const { onSearch } = useContext(JobsContext);
  const [state, setState] = useState({
    description: '',
    location: '',
    full_time: false
  });

  const handleInputChange = (event) => {
    const { name, value } = event.target;
    if (name === 'full_time') {
      setState((prevState) => ({ ...state, [name]: !prevState.full_time }));
    } else {
      setState({ ...state, [name]: value });
    }
  };

  const handleSearch = (event) => {
    event.preventDefault();
    console.log(state);
    onSearch(state);
  };

  return (
    <div className="search-section">
      <Form className="search-form" onSubmit={handleSearch}>
        <Row>
          <Col>
            <Form.Group controlId="description">
              <Form.Control
                type="text"
                name="description"
                value={state.description || ''}
                placeholder="Enter search term"
                onChange={handleInputChange}
              />
            </Form.Group>
          </Col>
          <Col>
            <Form.Group controlId="location">
              <Form.Control
                type="text"
                name="location"
                value={state.location || ''}
                placeholder="Enter location"
                onChange={handleInputChange}
              />
            </Form.Group>
          </Col>
          <Col>
            <Button variant="primary" type="submit" className="btn-search">
              Search
            </Button>
          </Col>
        </Row>
        <div className="filters">
          <Form.Group controlId="full_time">
            <Form.Check
              type="checkbox"
              name="full_time"
              className="full-time-checkbox"
              label="Full time only"
              checked={state.full_time}
              onChange={handleInputChange}
            />
          </Form.Group>
        </div>
      </Form>
    </div>
  );
};

export default Search;
Enter fullscreen mode Exit fullscreen mode

Now, let’s use the context in Results component

Remove both the props passed to the component

Import context at the top of the file

import JobsContext from '../context/jobs';
Enter fullscreen mode Exit fullscreen mode

Take the required values out of the context

const { results } = useContext(JobsContext);
Enter fullscreen mode Exit fullscreen mode

Now, you can remove the onItemClick prop passed to JobItem component

import React, { useContext } from 'react';
import JobItem from './JobItem';
import JobsContext from '../context/jobs';
const Results = () => {
  const { results } = useContext(JobsContext);
  return (
    <div className="search-results">
      {results.map((job, index) => (
        <JobItem key={job.id} {...job} index={index} />
      ))}
    </div>
  );
};
export default Results;
Enter fullscreen mode Exit fullscreen mode

Now, let’s refactor the JobDetails component

Import context at the top of the file

import JobsContext from '../context/jobs';
Enter fullscreen mode Exit fullscreen mode

Take the required values out of the context

const { details, onResetPage } = useContext(JobsContext);
Enter fullscreen mode Exit fullscreen mode

Now, your JobDetails.js file will look like this

import React, { useContext } from 'react';
import JobsContext from '../context/jobs';

const JobDetails = () => {
  const { details, onResetPage } = useContext(JobsContext);
  const {
    type,
    title,
    description,
    location,
    company,
    company_url,
    company_logo,
    how_to_apply
  } = details;

  return (
    <div className="job-details">
      <div className="back-link">
        <a href="/#" onClick={onResetPage}>
          &lt;&lt; Back to results
        </a>
      </div>
      <div>
        {type} / {location}
      </div>
      <div className="main-section">
        <div className="left-section">
          <div className="title">{title}</div> <hr />
          <div
            className="job-description"
            dangerouslySetInnerHTML={{ __html: description }}
          ></div>
        </div>
        <div className="right-section">
          <div className="company-details">
            <h3>About company</h3>
            <img src={company_logo} alt={company} className="company-logo" />
            <div className="company-name">{company}</div>
            <a className="company-url" href={company_url}>
              {company_url}
            </a>
          </div>
          <div className="how-to-apply">
            <h3>How to apply</h3>
            <div dangerouslySetInnerHTML={{ __html: how_to_apply }}></div>
          </div>
        </div>
      </div>
    </div>
  );
};

export default JobDetails;
Enter fullscreen mode Exit fullscreen mode

Now, let’s refactor the JobItem component

Import context at the top of the file

import JobsContext from '../context/jobs';
Enter fullscreen mode Exit fullscreen mode

Take the required values out of the context

const { onItemClick } = useContext(JobsContext);
Enter fullscreen mode Exit fullscreen mode

Now, your JobItem.js file will look like this

import React, { useContext } from 'react';
import moment from 'moment';
import JobsContext from '../context/jobs';

const JobItem = (props) => {
  const { onItemClick } = useContext(JobsContext);
  const {
    id,
    type,
    created_at,
    company,
    location,
    title,
    company_logo,
    index
  } = props;

  return (
    <div className="job-item" index={index + 1} onClick={() => onItemClick(id)}>
      <div className="company-logo">
        <img src={company_logo} alt={company} width="100" height="100" />
      </div>
      <div className="job-info">
        <div className="job-title">{title}</div>
        <div className="job-location">
          {location} | {type}
        </div>
        <div className="company-name">{company}</div>
      </div>
      <div className="post-info">
        <div className="post-time">
          Posted {moment(new Date(created_at)).fromNow()}
        </div>
      </div>
    </div>
  );
};

export default JobItem;
Enter fullscreen mode Exit fullscreen mode

Now, check your application and you can see that application works the same as previously but now we have avoided the unnecessary prop drilling and made code easier to understand

You can find code until this point HERE

Reset Scroll Position

One thing you might have noticed is that, when we scroll down a bit on the jobs list and click on any of the job, the page scroll remains at the same place and we see the bottom of the page instead of the top

scroll issue

This is because we are just adding hide class to components that are not needed when we click on any job so the scroll position does not change.

To fix this, open JobDetail.js file and add the following code

useEffect(() => {
  window.scrollTo(0, 0);
}, []);
Enter fullscreen mode Exit fullscreen mode

So now, when the JobDetails component is displayed, we are automatically displayed top of the page.

The empty array specifies that this code should be executed only when the component is mounted (similar to componentDidMount lifecycle method) and never again.

We also need to make sure that, the JobDetails component is only loaded when we click on any of the job so open HomePage.js file and change

<div className={`${page === 'home' && 'hide'}`}>
  <JobDetails />
</div>
Enter fullscreen mode Exit fullscreen mode

to

<div className={`${page === 'home' && 'hide'}`}>
  {page === 'details' && <JobDetails />}
</div>
Enter fullscreen mode Exit fullscreen mode

Now, if you check the application, you can see that the top of the page is displayed when clicked on any job.

Adding Load More Functionality

As we already know, we are getting only the latest 50 jobs when we hit the Github Jobs API, to get more jobs, we need to pass the page query parameter with an incremented number so let’s implement the load more functionality into our application.

Let's create a pageNumber state variable in HomePage.js with an initial value of 1 and selection state variable

const [pageNumber, setPageNumber] = useState(1);
const [selection, setSelection] = useState(null);
Enter fullscreen mode Exit fullscreen mode

Add the code to show the load more button in HomePage.js file

{
  results.length > 0 && _.isEmpty(errors) && (
    <div className="load-more" onClick={isLoading ? null : handleLoadMore}>
      <button disabled={isLoading} className={`${isLoading ? 'disabled' : ''}`}>
        Load More Jobs
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Also, move the loading condition from before the to after it

So your JSX returned form HomePage.js will look like this

return (
  <JobsContext.Provider value={value}>
    <div className={`${page === 'details' && 'hide'}`}>
      <Header /> <Search />
      {!_.isEmpty(errors) && (
        <div className="errorMsg">
          <p>{errors.error}</p>
        </div>
      )}
      <Results />
      {isLoading && <p className="loading">Loading...</p>}
      {results.length > 0 && _.isEmpty(errors) && (
        <div className="load-more" onClick={isLoading ? null : handleLoadMore}>
          <button
            disabled={isLoading}
            className={`${isLoading ? 'disabled' : ''}`}
          >
            Load More Jobs
          </button>
        </div>
      )}
    </div>
    <div className={`${page === 'home' && 'hide'}`}>
      {page === 'details' && <JobDetails />}
    </div>
  </JobsContext.Provider>
);
Enter fullscreen mode Exit fullscreen mode

In the add more button div above, we are disabling the button once the user clicks on it by adding the disabled class and disabled attribute

className={`${isLoading ? 'disabled' : ''}`}
Enter fullscreen mode Exit fullscreen mode

We are also making sure that the handleLoadMore function will not be executed when button is disabled so it's disabled by returning null from the onClick handler. This is useful in case the user removes the disabled attribute by editing it in dev tool.

Now add the handleLoadMore function inside the HomePage component

const handleLoadMore = () => {
  loadJobs({ ...selection, page: pageNumber + 1 });
  setPageNumber(pageNumber + 1);
};
Enter fullscreen mode Exit fullscreen mode

Now, we are passing the incremented page number to loadJobs function but we need to further pass it to our action dispatcher function so inside the loadJobs function just before dispatch(resetErrors()); add the following code

let isLoadMore = false;
if (selection.hasOwnProperty('page')) {
  isLoadMore = true;
}
Enter fullscreen mode Exit fullscreen mode

and pass the isLoadMore as the last parameter to initiateGetJobs function.
So your loadJobs function will look like this

const loadJobs = (selection) => {
  const { dispatch } = props;
  const { description, location, full_time, page = 1 } = selection;
  let isLoadMore = false;
  if (selection.hasOwnProperty('page')) {
    isLoadMore = true;
  }
  dispatch(resetErrors());
  setIsLoading(true);
  dispatch(
    initiateGetJobs({ description, location, full_time, page }, isLoadMore)
  )
    .then(() => {
      setIsLoading(false);
    })
    .catch(() => setIsLoading(false));
};
Enter fullscreen mode Exit fullscreen mode

and inside the function handleSearchction, call the setSelection function for setting the state

const handleSearch = (selection) => {
  loadJobs(selection);
  setSelection(selection);
};
Enter fullscreen mode Exit fullscreen mode

Now, open actions/jobs.js file and accept the isLoadMore as the second parameter

export const initiateGetJobs = (data, isLoadMore) => {
Enter fullscreen mode Exit fullscreen mode

and change

return dispatch(setJobs(sortedJobs));
Enter fullscreen mode Exit fullscreen mode

to

if (isLoadMore) {
  return dispatch(setLoadMoreJobs(sortedJobs));
} else {
  return dispatch(setJobs(sortedJobs));
}
Enter fullscreen mode Exit fullscreen mode

In this code, If the load more button is clicked then we are calling setLoadMoreJobs function to add new jobs to already existing results array.

If isLoadMore is false means we clicked on the Search button on the page then we are calling setJobs function to add the results in a new array.

Now, restart the React application by running yarn run start command and you can see that load more functionality is working as expected.

load more

You can find code until this point HERE

Creating Custom Loader Component For Overlay

But one thing you will notice is that we have moved the loading message to above the load more button so if we are entering some values in description and location fields when the results are already displayed and we click on Search button, we will not see the loading message because, for that, we need to scroll the page. This is not good user experience.

Also even though loading message is displayed, the user can click on any of the job even when loading is going on, which is also not expected.
So let’s create our own loader using React Portal to display the overlay so the user will not be able to click on any of the job when loading and we will also see a clear indication of loading.

If you are not aware of React Portal, check out my previous article HERE

Create a new file Loader.js inside components folder with the following content

import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';

const Loader = (props) => {
  const [node] = useState(document.createElement('div'));
  const loader = document.querySelector('#loader');

  useEffect(() => {
    loader.appendChild(node).classList.add('message');
  }, [loader, node]);

  useEffect(() => {
    if (props.show) {
      loader.classList.remove('hide');
      document.body.classList.add('loader-open');
    } else {
      loader.classList.add('hide');
      document.body.classList.remove('loader-open');
    }
  }, [loader, props.show]);

  return ReactDOM.createPortal(props.children, node);
};

export default Loader;
Enter fullscreen mode Exit fullscreen mode

Now open public/index.html and alongside the div with id root add another div with id loader

<div id="root"></div>
<div id="loader"></div>
Enter fullscreen mode Exit fullscreen mode

The ReactDOM.createPortal method which we have used in Loader.js will create a loader inside the div with id loader so it will be outside out React application DOM hierarchy and hence we can use it to provide an overlay for our entire application. This is the primary reason for using the React Portal for creating a loader.

So even if we will include the Loader component in HomePage.js file, it will be rendered outside all the divs but inside the div with id loader.

In the Loader.js file, we have first created a div where will add a loader message

const [node] = useState(document.createElement('div'));
Enter fullscreen mode Exit fullscreen mode

Then, we are adding the message class to that div and adding that div to the div added in index.html

document.querySelector('#loader').appendChild(node).classList.add('message');
Enter fullscreen mode Exit fullscreen mode

and based on the show prop passed from the HomePage component, we will add or remove the hide class and then finally we will render the Loader component using

ReactDOM.createPortal(props.children, node);
Enter fullscreen mode Exit fullscreen mode

Then we add or remove the loader-open class to the body tag of the page which will disable or enable the scrolling of the page

document.body.classList.add('loader-open');
document.body.classList.remove('loader-open');
Enter fullscreen mode Exit fullscreen mode

Here, the data we will pass in between the opening and closing Loader tag will be available inside props.children so we can display a simple loading message or we can include an image to be shown as a loader.

Now, let’s use this component

Open HomePage.js file and after the <JobsContext.Provider value={value}> line add the Loader component

<Loader show={isLoading}>Loading...</Loader>
Enter fullscreen mode Exit fullscreen mode

Also, import the Loader at the top of the file

import Loader from './Loader';
Enter fullscreen mode Exit fullscreen mode

Now, you can remove the previously used below line

{
  isLoading && <p className="loading">Loading...</p>;
}
Enter fullscreen mode Exit fullscreen mode

Now, when we will stop loading more items?
Obviously when there are no more items.

The Github Jobs API returns an empty array [] in response when there are no more jobs which you can check by passing larger page number to the API HERE

So to handle that open HomePage.js file and in loadJobs function, inside .then handler add following code

if (response && response.jobs.length === 0) {
  setHideLoadMore(true);
} else {
  setHideLoadMore(false);
}
setIsLoading(false);
Enter fullscreen mode Exit fullscreen mode

So your loadJobs function will look like this

const loadJobs = (selection) => {
  const { dispatch } = props;
  const { description, location, full_time, page = 1 } = selection;
  let isLoadMore = false;
  if (selection.hasOwnProperty('page')) {
    isLoadMore = true;
  }
  dispatch(resetErrors());
  setIsLoading(true);
  dispatch(
    initiateGetJobs({ description, location, full_time, page }, isLoadMore)
  )
    .then((response) => {
      if (response && response.jobs.length === 0) {
        setHideLoadMore(true);
      } else {
        setHideLoadMore(false);
      }
      setIsLoading(false);
    })
    .catch(() => setIsLoading(false));
};
Enter fullscreen mode Exit fullscreen mode

Add another state variable

const [hideLoadMore, setHideLoadMore] = useState(false);
Enter fullscreen mode Exit fullscreen mode

and for the load more button code, change

{results.length > 0 && _.isEmpty(errors) && (
Enter fullscreen mode Exit fullscreen mode

to

{results.length > 0 && _.isEmpty(errors) && !hideLoadMore && (
Enter fullscreen mode Exit fullscreen mode

So we just added an extra !hideLoadMore condition and now, if there are no more jobs coming from the response, we will hide the load more jobs button.

Now, if you check your application, you can see that the Load More Jobs button will not be displayed if there are no more jobs to load when we click on it. The beauty of including data to display in between the opening and closing Loader tag like this

<Loader show={isLoading}>Loading...</Loader>
Enter fullscreen mode Exit fullscreen mode

is that, we can include anything in between the tags even an image and that image will get displayed instead of the Loading text because we are using props.children to display inside the loader div using

ReactDOM.createPortal(props.children, node);
Enter fullscreen mode Exit fullscreen mode

You can find code until this point HERE

Adding Lazy Loading Images Functionality

As you are aware now when we are requesting from Jobs API, we are getting a list of 50 jobs initially and as we are showing the company logo on the list page, the browser has to download those 50 images which may take time so you might see the blank area sometimes before the image is fully loaded.

blank area

Also if you are browsing the application on a mobile device and you are using a slow network connection, it may take more time to download the images and those much MB of unnecessary images browser may download even if you are not scrolling the page to see other jobs listing which is not good user experience.

If you check the current functionality until this point when we click the Search button without entering any value, For me there are a total of 99 requests which took around 2MB of data.

request information

We can fix this by lazy loading the images. So until the user does not scroll to the job in the list, the image will not be downloaded which is more efficient.

So let’s start with it.

Create a new file observer.js inside custom-hooks folder with the following content

import { useEffect, useState } from 'react';

const useObserver = (targetRef) => {
  const [isVisible, setIsVisible] = useState(false);

  useEffect(() => {
    const observer = new IntersectionObserver((entries, observer) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          if (!isVisible) {
            setIsVisible(true);
          }
          observer.unobserve(entry.target);
        } else {
          setIsVisible(false);
        }
      });
    });

    const current = targetRef.current;
    observer.observe(current);

    return () => {
      observer.unobserve(current);
    };
  }, [isVisible, targetRef]);

  return [isVisible];
};

export default useObserver;
Enter fullscreen mode Exit fullscreen mode

In this file, we are using Intersection Observer API to identify which area of the page is currently displayed and only images in that area will be downloaded.

If you are not aware of the Intersection Observer, check out my previous article HERE which explains how to do lazy loading, sliding animation and play/pause video on a scroll in JavaScript in detail.

So in the observer.js file, we are taking a ref and adding that ref to be observed to the observer

observer.observe(current);
Enter fullscreen mode Exit fullscreen mode

If the image with added ref is displayed on screen then we are calling setIsVisible(true); and we are returning theisVisible value from this custom hook and based on theisVisible flag we can decide if we want to display the image or not.

So open JobItem.js file and add an import for the custom hook we created just now

import useObserver from '../custom-hooks/observer';
Enter fullscreen mode Exit fullscreen mode

Import useRef hook at the top of the file

import React, { useRef } from 'react';
Enter fullscreen mode Exit fullscreen mode

Create a ref which we can assign to the image

const imageRef = useRef();
Enter fullscreen mode Exit fullscreen mode

call the custom hook and get the isVisible value

const [isVisible] = useObserver(imageRef);
Enter fullscreen mode Exit fullscreen mode

change

<div className="company-logo">
  <img src={company_logo} alt={company} width="100" height="100" />
</div>
Enter fullscreen mode Exit fullscreen mode

to

<div className="company-logo" ref={imageRef}>
  {isVisible && (
    <img src={company_logo} alt={company} width="100" height="100" />
  )}
</div>
Enter fullscreen mode Exit fullscreen mode

Now, restart your React application by running yarn run start and check the lazy loading functionality.

lazy loading

As you can see initially only 5 requests are sent and only two logo images are downloaded and as you scroll the page, the next displayed images will be downloaded.

This is much better than the previous experience of downloading all the images at once. This will also load the page faster and save internet bandwidth.

You can find code until this point HERE

Adding Default Loading Image

If you noticed, even if we are loading the images lazily, initially you will see blank area instead of the image until the image is fully loaded.

blank area

We can fix this by providing an alternative image and replace it with the original image once it's completely downloaded.

This way we can avoid the empty space and is a widely used way of not showing the empty image area.

Download the loader image from HERE and add it Inside the src/images folder

The website used for creating the image is THIS.

You can specify the width, height, and text of the image you want.

The URL used to generate that loading image is this

https://via.placeholder.com/100x100?text=Loading
Enter fullscreen mode Exit fullscreen mode

Create a new file Image.js inside components folder with the following content

import React from 'react';
import { useState } from 'react';
import loading from '../images/loading.png';

/* https://via.placeholder.com/100x100?text=Loading */

const Image = ({ src, alt, ...props }) => {
  const [isVisible, setIsVisible] = useState(false);
  const changeVisibility = () => {
    setIsVisible(true);
  };

  return (
    <React.Fragment>
      <img
        src={loading}
        alt={alt}
        width="100"
        height="100"
        style={{ display: isVisible ? 'none' : 'inline' }}
        {...props}
      />
      <img
        src={src}
        alt={alt}
        width="100"
        height="100"
        onLoad={changeVisibility}
        style={{ display: isVisible ? 'inline' : 'none' }}
        {...props}
      />
    </React.Fragment>
  );
};

export default Image;
Enter fullscreen mode Exit fullscreen mode

In this file, we are initially displaying the loading image instead of the actual image.

The img tag has onLoad handler added which will be triggered when the image is completely loaded where we set the isVisible flag to true and once it's true we are displaying that image and hiding the previous loading image by using display CSS property.

Now open JobItem.js file and change

{
  isVisible && (
    <img src={company_logo} alt={company} width="100" height="100" />
  );
}
Enter fullscreen mode Exit fullscreen mode

to

{
  isVisible && (
    <Image src={company_logo} alt={company} width="100" height="100" />
  );
}
Enter fullscreen mode Exit fullscreen mode

Also, import Image component at the top of the file

import Image from './Image';
Enter fullscreen mode Exit fullscreen mode

Notice we have just changed img to Image and we are accessing the additional props in Image component as

const Image = ({ src, alt, ...props }) => {
Enter fullscreen mode Exit fullscreen mode

So except src and alt all other props like width, height will be stored in an array with the name props and then we are passing those props to the actual image by spreading the props array {...props} We can add the same functionality for the company logo on the details page.

Open JobDetails.js file and change

<img src={company_logo} alt={company} className="company-logo" />
Enter fullscreen mode Exit fullscreen mode

to

<Image src={company_logo} alt={company} className="company-logo" />
Enter fullscreen mode Exit fullscreen mode

Also, import the Image component at the top of the file

import Image from './Image';
Enter fullscreen mode Exit fullscreen mode

Now, restart your React application by running yarn run start and check it out

loading image

That's it about this article.

You can find complete Github source code for this application HERE and live demo HERE

Don't forget to subscribe to get my weekly newsletter with amazing tips, tricks and articles directly in your inbox here.

Top comments (43)

Collapse
 
miketalbot profile image
Mike Talbot ⭐

A huge effort went into that, for a new dev the step-by-step you've made is going to be very helpful.

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Thank you! It really means a lot...

Collapse
 
mugdhasonawane88 profile image
mugdhasonawane88

Nice article Yogesh !

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan

Thank you!

Collapse
 
snailcoder1 profile image
Boa Matule

Another great tutorial Yogesh! Thanks for sharing your knowledge with all of us.

Cheers

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Thank you so much!

Collapse
 
snailcoder1 profile image
Boa Matule • Edited

Mate, my server is not working! Trying debugging it but it seems i can't more forward! So localhost:5000 is giving me: Cannot GET /. Or from the Console: GET localhost:5000/ 404 (Not Found)

Help :)

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan • Edited

The jobs are not available on localhost:5000/ instead they are available on localhost:5000/jobs

which you can verify in server/server.js, line number 13

app.get('/jobs', async (req, res) => {

Let me know if it works

Thread Thread
 
snailcoder1 profile image
Boa Matule

Thanks loads mate!
I did actually managed to visualise them at localhost:5000/jobs but i thought i was doing something wrong because i am not able to display the results in the client server. Basically some errors in the /src/actions/jobs.js... Working on it. Thanks again for amazing work.

<3

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan

Okay. Great! If you read the article step by step then you will not get any error. You can always clone the final repository code from github.com/myogeshchavan97/github-... and compare your code with mine, in case you get any error.

Thread Thread
 
snailcoder1 profile image
Boa Matule

Stranger! Can't display the jobs cranky-haibt-9c4f4b.netlify.app/! Will check out what it's wrong.... !

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan • Edited

Netlify does not support Node.js apps directly. If you want to get your Node.js app deployed on Netlify, you need to deploy the App as a lambda function which I have explained in detail in this article.

Alternatively, you can clone my this repository where I have already done the configuration changes for Netlify.

You can check out my commits HERE to understand what changes I have done specific to Netlify

If you don't want to do such configuration, you can use Heroku to deploy the App because Heroku supports both React and Nodejs App directly.

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan

I have just published an article showing how to deploy this application to Heroku. Check it out HERE

Collapse
 
ammen1 profile image
Tamirat

good job

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Thank you🙏

Collapse
 
webdevrahul profile image
webdev-rahul

{
"error": "GitHub Jobs is deprecated! New jobs will not be posted from May 19, 2021. It will shut down entirely on August 19, 2021. Read more in: github.blog/changelog/2021-04-19-d..."
}

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Yes, the API will no longer work as its deprecated. But You can use the static data from the API which you can find here in the code repository.

You just need to return this JSON data from the backend API

Collapse
 
webdevrahul profile image
webdev-rahul

Really Appreciate your quick reply, also learned a lot from your Blogs and Courses. Great work

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan

Thank you! I'm glad you found the blog and my courses useful.

Collapse
 
paulmcaruana profile image
Paul Caruana

Great tutorial. When I click on the job I am not taken to the description and not sure why. Any thoughts?

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Thanks. Have you added the onClick Handler inside the components/JobItem.js file for the div like this?

<div className="job-item" index={index + 1} onClick={() => onItemClick(id)}>
Enter fullscreen mode Exit fullscreen mode

You also need to add the handleItemClick function inside the HomePage.js file like this

const handleItemClick = (jobId) => {
    setPage('details');
    setJobId(jobId);
  };
Enter fullscreen mode Exit fullscreen mode
Collapse
 
about_exam profile image
All About Sarkari Result Exam

First of all, I warmly welcome you. He has given very good information. For computer, internet, technology information, you can visit our website. You keep on giving similar information in future also. Which helps a lot to all of us.

Collapse
 
dermalfillers profile image
Dermal Fillers

We provides call center jobs in Rawalpindi and Islamabad. If you are looking for jobs, then visit us now. We are also providing part-time jobs for students and all at All-Star BPO.

Collapse
 
bhansa profile image
Bharat Saraswat

Hi, I am getting cors error while hitting the github api, did you face that issue?

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan • Edited

I think you're making API call from client side app. GitHub jobs API does not allow accessing jobs from client side apps. You need to make API call from server side only. That's why I have used Node.js for making API call

Collapse
 
bhansa profile image
Bharat Saraswat

Understood, thanks for the explanation.

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan

it's my pleasure

Thread Thread
 
theydonist404 profile image
owenm33 • Edited

EDIT: seems like the cause of this issue is that Github Jobs API is now deprecated (see here: github.blog/changelog/2021-04-19-d...)

Could somebody please elaborate on the fix for this? I ended up cloning the source repo and trying to access localhost:5000/jobs after yarn start from top-level and server folders (as instructed in readme) but I'm getting no UI in Firefox and console is reporting "Content Security Policy: The page’s settings blocked the loading of a resource at localhost:5000/favicon.ico (“default-src”)."

I've tried to follow the suggestions here: csplite.com/csp212/ but I'm just curious as to why I'm still getting this error even after copying the repo exactly.

Thanks heaps for the tutorial btw, it was super helpful

Collapse
 
josiahwilliams profile image
Josiah-williams

I don't if the api still works but it gives me an error on the console when i click the search button

GET localhost:5000/jobs?description=&l... net::ERR_CONNECTION_RESET

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

You need to be connected to the internet to access the jobs. Looks like your internet connection was down when you clicked the search button.

Collapse
 
josiahwilliams profile image
Josiah-williams

I have tried changing internet connectivity its still the same thing

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan • Edited

Have your started your server by running yarn start command from server folder?

Collapse
 
kene_nwobodo profile image
Kenechukwu Nwobodo • Edited

Hi great article. While using running the program in localhost, the list of jobs wasn't displaying in the UI for me. I checked console, was getting errors.

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

Can you post the error that you're getting? You can also check the live demo for working app here

Collapse
 
kene_nwobodo profile image
Kenechukwu Nwobodo

In the console this.
GET localhost:6000/jobs?description=Fr... net::ERR_UNSAFE_PORT

Happens anytime I click on search button in the UI

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan • Edited

Google chrome considers 6000 as an unsafe port. HERE is a list of unsafe ports which are not allowed by browser. Use any other port or 5000 as I have used in the project. Make sure to use the same port in src/utils/constants.js and server/server.js file also. You can anytime clone my repository from HERE to compare your code with mine and check if you missed anything.

Collapse
 
about_exam profile image
All About Sarkari Result Exam

Thanks for sharing your knowledge with all of us.

ExamMedia.in Gives Information For Sarkari Naukri, Sarkari Exam, Sarkari Result, Sarkariexam, Latest Govt. Jobs Notifications.

Find all jobs - Exammedia.in

Collapse
 
agnessafilary profile image
Agnessa Filary

Hey there! I was always looking for a lot of various ways about how to find a good job. As for me, I checked a lot of possible solutions and remote jobs seems to be a really updated solution. There you can easily find a lot of possible job options and the way in order how to apply for a good job. It is playing a really important role for me.

Collapse
 
dinah001 profile image
Dinah Halpert

I found your article simply amazing. It's a fantastic resource for anyone diving into React development. Building a job search app using Github's API provides hands-on experience with essential React concepts, like lazy loading images, context sharing, and even addressing HTML rendering challenges. It's an informative guide for both beginners and those looking to expand their React skill set. Kudos to the author! For career-oriented folks, this learning journey may even lead to seeking Chicago resume writing services to land that dream job.

Collapse
 
primelos profile image
carlos fabian venegas

dangerouslySetInnerHTML did not work for me? so i used
npm i html-react-parser. That solved my html problem.

Collapse
 
myogeshchavan97 profile image
Yogesh Chavan

It should work with dangerouslySetInnerHTML. Are you sure you have used double underscores like this: __html?

Collapse
 
primelos profile image
carlos fabian venegas

I did check, even copied from you repo. After I completed your project, I went back tried it again and now it works? I am new so I wish I understood why it didn't work the first time, but at least I found a alternative working solution.
thank you

Thread Thread
 
myogeshchavan97 profile image
Yogesh Chavan

That's strange. Glad to hear that it's working now