2022-06-02T18:25:35Z

The React Mega-Tutorial, Chapter 5: Connecting to a Back End

In this chapter you are going to learn how the React application can communicate with a back end application to request data. While doing this, you will learn how to use the two most important React hooks: useState() and useEffect().

The complete course, including videos for every chapter is available to order from my courses site. Ebook and paperback versions of this course are also available from Amazon. Thank you for your support!

For your reference, here is the complete list of articles in this series:

Running the Microblog API Back End

Starting in this chapter, you will need to run the project's companion back end application. This is an open source application called Microblog API that provides all the storage and authentication functionality needed by the React front end you are building.

Microblog API has a read me page that you can consult for detailed installation instructions, but the following sections cover the basic installation steps for three different installation methods.

Getting an Email Server

The Microblog API service needs access to an email server, to be used in a later chapter while implementing the reset password flow, which requires the server to send emails.

If you don't have an email service that you can use, my recommendation is that you open a SendGrid account. The free tier of this service includes 100 emails per day, which is more than enough for the needs of this project.

To configure the email service in Microblog API, you will need to provide values for the several configuration variables:

  • MAIL_SERVER: The email server to use when sending emails.
  • MAIL_PORT: The port in which the email server listens for connections.
  • MAIL_USE_TLS: Set to any non-empty string to use an encrypted connection.
  • MAIL_USERNAME: The username for the email sender's account.
  • MAIL_PASSWORD: The password for the email sender's account.
  • MAIL_DEFAULT_SENDER: The email address that appears in all emails sent by the application as the sender.

If you decide to use SendGrid, open an account, and then create an API key. The email settings for a SendGrid account are as follows:

MAIL_SERVER=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=apikey    # <-- this is the literal word "apikey"
MAIL_PASSWORD=          # <-- your SendGrid API key here
MAIL_DEFAULT_SENDER=    # <-- the sender email address you'd like to use

As noted above in comments, the MAIL_PASSWORD variable needs to be set to your SendGrid API key. The MAIL_DEFAULT_SENDER can be set to any email address. This address is going to be in the From field of all emails sent by the service.

Installing the Back End

In this section, three methods of installation are described:

  1. Run on your computer with Docker
  2. Run on your computer with Python
  3. Deploy to a free or paid Heroku account

Which is the best method? If you are familiar with Docker containers, then use option 1. If you are familiar with setting up and running Python applications on your computer, then option 2 should be relatively easy for you to implement, but if you don't want to complicate yourself with running a service on your computer, then Heroku might be the best option for you. You can review the following sections to learn what's involved in each of the methods if you need more information to decide.

Running the Back End on Docker

If you are interested in running the back end service as a Docker container, you need to have Docker and git installed.

Open a new terminal window and find a suitable parent directory that is outside the React project directory. Clone the Microblog API GitHub repository to download the project to your computer:

git clone https://github.com/miguelgrinberg/microblog-api
cd microblog-api

Create a configuration file with the name .env. An example is shown below, assuming you are using a SendGrid account as email server. Make sure you enter the appropriate email server details that apply to you.

DISABLE_AUTH=true
MAIL_SERVER=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=apikey
MAIL_PASSWORD=                 # <-- your SendGrid API key
MAIL_DEFAULT_SENDER=           # <-- any email address

It is important that the DISABLE_AUTH variable is set to true at this time, to remove authentication. You will enable authentication support in a later chapter.

Start the back end with Docker Compose as follows:

docker-compose up -d

Once the service is up and running, run the next two commands to populate the database used by the service with some randomly generated data:

docker-compose run --rm microblog-api bash -c "flask fake users 10"
docker-compose run --rm microblog-api bash -c "flask fake posts 100"

To check that the service is running correctly, navigate to the http://localhost:5000 URL on your web browser. This should open the live API documentation site.

On some computers, port 5000 might be in used by another service. If that is your situation, you can specify a different port by adding a MICROBLOG_API_PORT variable to the .env file. The following example configures the service to run on port 4000:

MICROBLOG_API_PORT=4000

Running the Back End with Python

If you are familiar with running Python applications, you can just install and run the application directly on your computer. For this you need to have a recent Python 3 interpreter and git installed.

Open a new terminal window, find a location outside the React project's directory, and clone the GitHub repository for Microblog API there:

git clone https://github.com/miguelgrinberg/microblog-api
cd microblog-api

As with Docker, you need to create a configuration file with the name .env. Below you can see an example configuration file that assumes you are using a SendGrid account as email server.

DISABLE_AUTH=true
MAIL_SERVER=smtp.sendgrid.net
MAIL_PORT=587
MAIL_USE_TLS=true
MAIL_USERNAME=apikey
MAIL_PASSWORD=                 # <-- your SendGrid API key
MAIL_DEFAULT_SENDER=           # <-- any email address

It is very important that the DISABLE_AUTH variable in this configuration file is set to true at this time, to allow this service to work without authentication. You will enable authentication later, when implementing user logins.

Create a Python virtual environment and install the project's dependencies in it:

python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

Initialize the service's database and add some random content to it with the following commands, which must be executed while the virtual environment is activated:

flask db upgrade
flask fake users 10
flask fake posts 100

To start the service type the following command in your terminal:

flask run

With the service running, navigate to http://localhost:5000 and make sure the live API documentation site opens.

If you need to run the service on a different port, add the --port option to the run command. For example, to use port 4000, the command is:

flask run --port=4000

Deploying the Back End to Heroku

If you want to deploy to the Heroku service, first make sure you have an account on this service. A free account is sufficient for the needs of this project.

The read me page for the Microblog API project has a "Deploy to Heroku" button. Click this button to configure the deployment.

In the "App name" field you have to enter a name for the deployed back end application. You will need to find a name that hasn't been used by any other Heroku customer, so it may take a few tries until you find a unique name that is accepted.

For the "Choose a region" dropdown you can pick a region that is closest to where you are located, or you can leave the default selection.

In the DISABLE_AUTH field, enter the word true. This is going to deploy the service with authentication disabled. Authentication is covered in a later chapter, for now you will need to have the service running without authentication.

The next set of fields are for the email settings. Above you can find the settings you have to use if you followed my recommendation of using a SendGrid account. If you use a different email provider, configure it as required by your service.

You can leave any additional fields set to their default values.

When you are done with the configuration, click the "Deploy app" button. The deployment should only take a minute or two. When the application is deployed, you should see all green check marks, as shown in Figure 5.1.

Figure 5.1: A completed Heroku deployment

The deployed application can be accessed at the URL https://APPNAME.herokuapp.com, where APPNAME is the application name that you entered in the configuration form. For example, if you deployed the application with the name "susan-microblog-api", then the base URL for the application is https://susan-microblog-api/herokuapp.com.

To make sure the application has been deployed successfully, you can open the base URL of your deployment in your browser. This should open the live documentation page for the service.

Using State Variables

The Posts component renders fake blog posts that were manually entered into the code early in the life of the project. This was useful because it made it possible to make progress with other parts of the project, without having to complicate things by involving the back end from the very start. Now it is finally time to remove the fake posts and render content returned by the server.

Doing this presents some challenges. For performance reasons, React requires that component functions render themselves to JSX quickly. In particular, it is not allowed for the render function to send a request out to the server asking for data and blocking the render while waiting for a response, as this would cause the whole application to freeze and appear unresponsive to the user.

How can the data be requested then? The short answer is that this is done as a side effect, but you will learn what this means in the next section. For now let's just say that the component has to schedule the request to run as a background task, so that the render process isn't held back.

The process of rendering data obtained from the server takes a few steps:

  1. The component's render function is invoked. Within this function, the request to the server is scheduled as a background operation (called a side effect, in React jargon). The component function must return quickly and without blocking, so at this point it renders without any data, usually showing a spinner image or "loading" message.
  2. The background task that was scheduled to request the data runs, and at some point a response from the server is received. The background task notifies React that some new data has arrived and is ready for rendering.
  3. React calls the component's render function a second time, and the component re-renders itself with the data that was received.

After you think about this process, you will likely wonder how do the background data retrieval function, React and the rendering component communicate and coordinate to perform the three steps outlined above.

The React feature that makes this multistep render process possible is called a state variable.

The useState() hook function from React is used to create state variables. Hooks are special functions that have names that start with the word use. These functions can only be called from component functions or from other hooks. Calling a hook in any other context is not allowed and will be reported as an error when the application is built.

Here is how a posts state variable to hold a list of blog posts could be allocated inside the Posts component:

const [posts, setPosts] = useState();

If an argument is passed to useState(), then this becomes the initial value for the state variable. This can be a primitive value such as a string or a number, it can also be an array or an object, and it can also be set to null or undefined. If the argument is omitted, then the state variable is initialized with a value of undefined.

The return value from the hook is an array with two elements, which in the above example are assigned to two constants using a destructuring assignment.

The first element of the returned array is the current value of the state variable. Going back to the three rendering steps above, when the component renders for the first time in step 1, this would be the initial value assigned to the state variable, which in this case is undefined.

The second element of the array is a setter function for the state variable. This function must be used to update the value of the state variable. This will be done in the background task when the data is received. Calling the setter function with a new value is what allows React to trigger the render that occurs in step 3 above.

When the component renders a second time in step 3, the posts constant returned by useState() is going to have the value that was passed to the setter function.

The first step in the migration to use real data in the Posts component is to replace the posts constant and the fake blog posts with a state variable of the same name.

src/components/Posts.js: Add a posts state variable

import { useState } from 'react';
import Spinner from 'react-bootstrap/Spinner';

export default function Posts() {
  const [posts, setPosts] = useState();

  // TODO: add a side effect function to request posts here

  return (
    <>
      {posts === undefined ?
        <Spinner animation="border" />
      :
        <>
          ... // <-- no changes to blog post JSX
        </>
      }
    </>
  );
}

The first line imports the useState() hook from the React library. The Spinner component imported in the second line comes with React-Bootstrap.

The state variable is created in the first line of the component function. The comment that follows is a placeholder for the side effect function, which will be added in the next section.

The return statement that looped through the blog posts has been expanded. At the top level there is a conditional that checks if the posts state variable is set to undefined, and in that case a spinner component is returned. This implements step 1 of the render process described earlier in this section, when the data from the server isn't available yet.

If posts has a value other than undefined, then it means that this is the second render, so in that case the previous loop logic is used to render the data obtained from the server.

Figure 5.2 shows how the application looks with these last changes.

Figure 5.2: A spinner while the component's state variable is undefined

Side Effect Functions

In this section you are going to send a request to the back end to obtain remote data, which will be rendered to the page with the state variable logic added in the previous section. There is a bit of preparation required before the request can be made.

Configuring the Back End Root URL

Before the front end can make a request to the server, it needs to know what is the root URL of your back end. Depending on your back end installation method here is how you can determine this URL:

  • If you are running the back end on a local Docker, then your root URL is http://localhost:5000. If the Docker host is remote, then change localhost to the IP address or hostname of your Docker host. When using a custom port, change the 5000 to your chosen port number.
  • If you are running the Python application directly on your computer, then your root URL is also http://localhost:5000. Once again, be sure to change the 5000 to the correct number if you are using a custom port.
  • If you are running the back end on Heroku, then your root URL is https://APPNAME.herokuapp.com, where APPNAME is the application name that you selected when you deployed the service. If you can't remember the name, you can visit your Heroku dashboard to look it up.

To check that you have the correct root URL for your back end, type this URL in your web browser. If the URL is correct, the browser should automatically redirect to /docs and show you the API documentation site. You can see how this looks in Figure 5.3.

Figure 5.3: Microblog API documentation site

The most convenient way to incorporate the back end URL into the front end project is through an environment variable. Applications generated by Create React App have environment variable support, either directly from the shell or through environment files.

Create an environment file named .env (note the leading dot) in the top-level directory of your React project, and enter the following line in it, editing the URL to be the correct one for your back end installation:

REACT_APP_BASE_API_URL=http://localhost:5000

Do not include a trailing slash when you enter the back end URL.

Environment variables that are meant to be imported into the React application must have a name that starts with REACT_APP_. This ensures that any other variables that are in your environment, some of which can be sensitive, are not leaked to the front end by mistake.

Save the .env file with the new variable. Then stop the npm start process with Ctrl-C and restart it, so that the environment file is imported. The Create React App build will make the variable accessible anywhere in the application as process.env.REACT_APP_BASE_API_URL.

Using the API Documentation Site

Open the Microblog API documentation site once again, by typing its root URL in a browser. Find the "Endpoints" section in the left sidebar, and open the "posts" subsection to see all the endpoints related to blog posts. Click on the endpoint labeled "Retrieve the user's post feed".

The documentation page for the endpoint shows how to make this request. Because the back end is deployed without authentication support, you can ignore the security aspects for now. When running without authentication, all requests are automatically authenticated to the first user in the system.

From this page you can learn that the HTTP method for this request is GET, and that the URL path to attach after the root server URL is /api/feed. There are some query parameters related to pagination, but they are all optional, so there is no need to worry about those for now.

The "Responses" section shows the structure of the data that is returned by the request, which has data and pagination top-level sections.

On the right side of the page there is a web form, where you can enter input arguments for the request and send a test request out to the server. The only input argument this endpoint requires is the authentication token, but it is currently not checked, so you can click the "Send API Request" button with all the input fields blank and see an example response from this endpoint. This will allow you to familiarize yourself with a real response, which should include some randomly generated users and blog posts.

Sending a Request with fetch()

The fetch() function, available in all modern web browsers, is the simplest way to send HTTP requests to a server. Another popular HTTP client used in many React applications is axios. For this application fetch() will be used.

GET requests that don't require authentication or other input arguments can be sent just by calling fetch() with the URL of the target endpoint as an argument. For example:

const BASE_API_URL = process.env.REACT_APP_BASE_API_URL;
const response = await fetch(BASE_API_URL + '/api/feed');

The fetch() function uses promises, so it needs to be awaited when you are in a function declared as async.

The Response object returned by fetch() provides many attributes and methods to work with the HTTP response. A very useful attribute is response.ok, which is true when the request returned a success status code. For a JSON API, the response.json() method parses the data in the body of the response and returns it as a JavaScript object or array:

const results = await response.json();
if (response.ok) {
  console.log(results.data);
  console.log(results.pagination);
}

In this example, the response.json() method (which also returns a promise) is awaited, and then if the request was successful, the data and pagination keys of the JSON payload are printed to the console.

Creating a Side Effect Function

In React, side effect functions are created with the useEffect() hook function inside the component's render function. The first argument to useEffect() is a function with the code that needs to run in the background. The second argument is an array of dependencies that determine when the effect needs to run.

Understanding how to use the second argument to useEffect() is often difficult when learning React. A simple rule to remember, is that when this argument is set to an empty array, the side effect function runs once when the component is first rendered and never again.

A common mistake is to forget to include the second argument. This is interpreted by React as instructions to run the side effect function every single time the component renders, which is rarely necessary.

Later you will see that there are cases that require specific values for this array, but it is best to get used to set this argument to an empty array and then changing it only when necessary.

The listing below shows the updated Posts component, with the side effect function in place.

src/components/Posts.js: Load blog posts as a side effect

import { useState, useEffect } from 'react';
import Spinner from 'react-bootstrap/Spinner';

const BASE_API_URL = process.env.REACT_APP_BASE_API_URL;

export default function Posts() {
  const [posts, setPosts] = useState();

  useEffect(() => {
    (async () => {
      const response = await fetch(BASE_API_URL + '/api/feed');
      if (response.ok) {
        const results = await response.json();
        setPosts(results.data);
      }
      else {
        setPosts(null);
      }
    })();
  }, []);

  return (
    <>
      {posts === undefined ?
        <Spinner animation="border" />
      :
        <>
          {posts === null ?
             <p>Could not retrieve blog posts.</p>
          :
            <>
              ... // <-- no changes to blog post JSX
            </>
          }
        </>
      }
    </>
  );
}

The side effect function is defined after the posts state variable, so that it can access the setPosts() setter function to update the list of posts.

React requires that the function that is given as the argument to useEffect() is not async. A commonly used trick to enable the use of async and await in side effect functions is to create an inner async function and immediately call it. This pattern is commonly referred as an Immediately Invoked Function Expression (IIFE). To help you understand this, here is how this inner async function looks, isolated from the rest of the code:

(async () => {
  // await can be used here
})();

Note how the arrow function is defined within parenthesis, and the () at the end invokes it.

The logic in the side effect function uses fetch() to retrieve the user's post feed from the server. If the request succeeds, then the list of posts is set in the posts state variable through the setPosts() setter function.

When the state variable changes, React will trigger a new render of the component, and this time the loop section of the JSX will be used.

To make this component robust, it is also necessary to handle the case of the request failing. In the side effect function, the value of the posts state variable is set to null when response.ok is false.

As discussed above, the second argument to useEffect() is set to [], to indicate that the function given in the first argument should only run when the initial render occurs. In this first render, the component will render itself with the spinner.

The JSX contents of this component have been expanded to also handle the case of posts being null, which is handled by rendering an error message. In a later chapter you will learn how to create global error alerts, which is better than having them in every component that uses the API.

Figure 5.4 shows actual blog posts returned by the server rendered on the page. If you are wondering why the content does not make any sense, remember that during installation, the back end's database was filled with randomly generated content.

Figure 5.4: Blog posts rendered on the feed page

Rendering Blog Posts

Displaying more content makes it obvious that the render code for posts is extremely plain and needs an improvement. The code for the Posts component has grown as a result of adding the state variable and the side effect function, so this is a good opportunity to refactor it, by moving the JSX loop that renders a single blog post into a separate component, which can be called Post.

Here is Posts after the blog post rendering code was refactored:

src/components/Posts.js: Simplified Posts component

import { useState, useEffect } from 'react';
import Spinner from 'react-bootstrap/Spinner';
import Post from './Post';

const BASE_API_URL = process.env.REACT_APP_BASE_API_URL;

export default function Posts() {
  // <-- no changes to state variable and side effect function

  return (
    <>
      {posts === undefined ?
        <Spinner animation="border" />
      :
        <>
          {posts === null ?
             <p>Could not retrieve blog posts.</p>
          :
            <>
              {posts.length === 0 ?
                <p>There are no blog posts.</p>
              :
                posts.map(post => <Post key={post.id} post={post} />)
              }
            </>
          }
        </>
      }
    </>
  );
}

The loop in Posts is now a single line that references Post for each blog post. Something to remember when refactoring loops is that the required key attribute must always be in the source file that has the loop. React will not see it if it is moved into the child component.

The application is now broken, because Posts imports the Post component, which doesn't exist yet. The code for Post, with several render improvements, is shown below.

src/components/Post.js: Styled blog posts

import Stack from 'react-bootstrap/Stack';
import Image from 'react-bootstrap/Image';
import { Link } from 'react-router-dom';

export default function Post({ post }) {
  return (
    <Stack direction="horizontal" gap={3} className="Post">
      <Image src={post.author.avatar_url + '&s=48'}
             alt={post.author.username} roundedCircle />
      <div>
        <p>
          <Link to={'/user/' + post.author.username}>
            {post.author.username}
          </Link>
          &nbsp;&mdash;&nbsp;
          {post.timestamp}:
        </p>
        <p>{post.text}</p>
      </div>
    </Stack>
  );
}

The new render code isn't very long, but there are a lot of changes from the previous version.

In the previous render logic, a post was rendered as a <p> element. This is not a good top-level element, because it makes it harder to also render the user's avatar image.

The new layout uses a horizontal Stack as the main component. The gap attribute of the stack adds a margin around each child. As with other top-level components, a class name of Post is added to facilitate CSS styling.

The first child in the stack is the Image component from React-Bootstrap, which is used to render the user's avatar image URL that is returned by the server. The server works with Gravatar URLs, which accept a s parameter in the query string to request a specific image size, set to 48 pixels in this instance. The roundCircle attribute makes the images round instead of square, which gives the avatar a nice touch. Microblog API returns Gravatar images that generate a geometric design for any email addresses that do not have a registered avatar. You can visit Gravatar to register an avatar for your email address.

The second component in this stack is the body of the post, which uses a <div> element as parent, with two <p> paragraphs inside for the post header and body respectively. The header uses a Link component from React-Router for the author's username. The link points to the /user/{username} user profile placeholder page created in Chapter 4.

Post Styling Improvements

The body of the post is rendered directly inside the second <p>. The resulting page needed a few minor CSS adjustments to look its best. Below are the CSS definitions added to index.css.

src/index.css: Styles for blog posts

... // <-- no changes to existing styles

.Content {
  margin-top: 10px;
}

.Post {
  align-items: start;
  padding-top: 5px;
  border-bottom: 1px solid #eee;
}

.Post:hover {
  background-color: #f8f8f8;
}

.Post a {
  color: #14c;
  text-decoration: none;
}

.Post a:visited {
  color: #14c;
}

In addition to some small spacing and alignment fixes, the CSS above changes the color and style of the username links, adds a border line between blog posts, and changes the background color of the post under the mouse pointer.

Displaying Relative Times

While the new Post component nicely formats blog posts on the page, the one part in which it is still lacking is in how the time of the post is presented. Microblog API returns all timestamps as strings that follow the ISO 8601 specification, which is a widely accepted format for machine-to-machine communication of dates and times. But this format is not the best for use by humans.

In this type of application, what works best is to show the time of a post in relative terms, such as "yesterday" or "3 hours ago". In this section you'll add a TimeAgo component that takes a timestamp in ISO 8601 format as a prop, and renders it to the page as a relative time. First update the Post component to use TimeAgo.

src/components/Post.js: Relative time for blog posts

import Stack from 'react-bootstrap/Stack';
import Image from 'react-bootstrap/Image';
import { Link } from 'react-router-dom';
import TimeAgo from './TimeAgo';

export default function Post({ post }) {
  return (
    <Stack direction="horizontal" gap={3} className="Post">
      <Image src={post.author.avatar_url + '&s=48'}
             alt={post.author.username} roundedCircle />
      <div>
        <p>
          <Link to={'/user/' + post.author.username}>
            {post.author.username}
          </Link>
          &nbsp;&mdash;&nbsp;
          <TimeAgo isoDate={post.timestamp} />:
        </p>
        <p>{post.text}</p>
      </div>
    </Stack>
  );
}

With the above change the application is once again temporarily broken, this time due to the reference to a nonexistent TimeAgo component.

The following listing shows the general structure that will be used for this component, with placeholders for the actual logic.

src/components/TimeAgo.js: Render relative times

import { useState, useEffect } from 'react';

const secondsTable = [
  ['year', 60 * 60 * 24 * 365],
  ['month', 60 * 60 * 24 * 30],
  ['week', 60 * 60 * 24 * 7],
  ['day', 60 * 60 * 24],
  ['hour', 60 * 60],
  ['minute', 60],
];
const rtf = new Intl.RelativeTimeFormat(undefined, {numeric: 'auto'});

function getTimeAgo(date) {
  // TODO
}

export default function TimeAgo({ isoDate }) {
  // TODO
}

The component is going to use the useState() and useEffect() hooks, this time for a purpose that is not related to loading remote resources from the network.

The secondsTable array has the number of seconds in a year, a month, a week, a day, an hour, and a minute. These numbers are going to be useful in determining which of these units is the best to use in the relative time.

The rtf constant is an instance of the Intl.RelativeTimeFormat class, which is going to generate the actual text of the relative time. This class is not well known, but is available in all modern browsers.

The getTimeAgo() helper function accepts a Date object and finds the best relative units to use to render it. The function is defined outside the TimeAgo component because it is a standalone function that does not need to be different for each instantiation of the component, and it also does not need to change when the component re-renders.

The implementation of the getTimeAgo() function is shown below.

src/components/TimeAgo.js: getTimeAgo() function

function getTimeAgo(date) {
  const seconds = Math.round((date.getTime() - new Date().getTime()) / 1000);
  const absSeconds = Math.abs(seconds);
  let bestUnit, bestTime, bestInterval;
  for (let [unit, unitSeconds] of secondsTable) {
    if (absSeconds >= unitSeconds) {
      bestUnit = unit;
      bestTime = Math.round(seconds / unitSeconds);
      bestInterval = unitSeconds / 2;
      break;
    }
  };
  if (!bestUnit) {
    bestUnit = 'second';
    bestTime = parseInt(seconds / 10) * 10;
    bestInterval = 10;
  }
  return [bestTime, bestUnit, bestInterval];
}

This function starts by calculating the number of seconds between the date argument and current time. For dates that are in the past, the result of this calculation is going to be negative, so for that reason the absSeconds constant stores the positive number of seconds.

A for-loop then iterates over the elements of secondsTable, to find the first unit (from largest to smallest) that is smaller than absSeconds, as this is the best relative unit to use. If a unit is found, then the function stores three related values to it:

  • bestUnit is the unit that was determined to be the best to use.
  • bestTime is the amount of time, in the selected units, rounded to the nearest integer.
  • bestInterval is the interval at which the relative time needs to be updated. Since all amounts are rounded, there is no reason to update more often than at half of the selected time unit.

If none of the units in secondsTable is selected, it means that the time that needs to be rendered is less than a minute old. In this case the units are set to 'second', but to avoid refreshing the time every second an interval of 10 seconds is used instead.

The return value of the function is an array with the amount of time, the unit, and the interval selected.

The implementation of the TimeAgo component is shown next.

src/components/TimeAgo.js: TimeAgo component

export default function TimeAgo({ isoDate }) {
  const date = new Date(Date.parse(isoDate));
  const [time, unit, interval] = getTimeAgo(date);
  const [, setUpdate] = useState(0);

  useEffect(() => {
    const timerId = setInterval(
      () => setUpdate(update => update + 1),
      interval * 1000
    );
    return () => clearInterval(timerId);
  }, [interval]);

  return (
    <span title={date.toString()}>{rtf.format(time, unit)}</span>
  );
}

Thanks to moving some logic to the getTimeAgo() helper function, this component is quite short, but there are several important React tricks hidden in this component that are worth discussing in detail.

The component first creates a Date object, by parsing the isoDate prop, which is the string representation of the post's date in ISO 8601 format. The getTimeAgo() function is called with this date to obtain the time, the units and the interval to use when rendering.

Let's ignore the state variable and the side effect function for a moment and look at the rendered JSX first. The component renders the timestamp as a <span> element. The text of the element is generated with the rtf.format() function, provided by the browser. This function takes the time and the units, and generates locale friendly text for that amount time. For example, when time = -1 and unit = 'day', this function might return the word 'yesterday' for a browser configured for English.

The <span> element also has a title attribute, which renders the date with its default string representation. Browsers create a tooltip with the content of this attribute, which can be viewed by hovering the mouse over the text of the element.

You now know how the relative times can be rendered, but in addition to rendering them it would be nice if they automatically updated as time passes. The state variable and side effect function take care of this.

The usage of useState() in this component is different from the previous one, because only the setter function is stored. This usage solves a very specific need that this component has, which is to force itself to re-render even though none of the inputs ever change. React only re-renders components when their props or state variables change, so the only way to force a re-render is to create a dummy state variable that is not used anywhere, but is changed when a re-render is needed.

The useEffect() hook is used to create a side effect function, as before. The function creates an interval timer that runs at the interval returned in the third array element of the getTimeAgo() helper function.

The interval function calls the setUpdate() setter function of the state variable. Previously you've seen that state variable setter functions take the new value of the variable as an argument. An alternative form for the setter that is useful when the current value of the state variable is unknown or out of scope, is to pass a function. With this usage, React calls the function, passing the current value of the variable as an argument, and the function must return the updated value. Since this state variable is only needed to cause an update, any value that is different from the current one works. In this component the value of the state variable is incremented by one every time an update needs to be triggered.

This side effect function returns a function as a result, as opposite to the one in the Posts component, which does not return anything. Sometimes, side effect functions allocate resources, such as the interval timer here, and these resources need to be released when the component is removed from the page, to avoid resource leaks. When a side effect function returns a function, React calls this function to allow the component to perform any necessary clean up tasks. For this component, the side effect clean up function cancels the interval timer.

The second argument to useEffect() is also different in this side effect. You've seen that a side effect function that has an empty dependency array runs only during the first render of the component. Using the "10 minutes ago" example discussed above, this initial run of the side effect function would set up an interval timer than runs every 30 seconds. The interval timer is useful to keep the timestamp updated, but eventually one of these re-renders will change the units from minutes to hours, and at that point having an interval timer every 30 seconds makes no sense anymore, as it would be better to reduce the frequency to every 30 minutes. By adding interval to the dependency array of the side effect, the component is asking React to run the side effect function not only during the first render, but also during any renders in which the value of interval changes from its previous value.

Side effect dependencies are hard to think about. To help you understand the sequence of events in the life of this component, here is an example description of renders and side effect runs:

  • Let's assume the initial render happens 10 minutes and 20 seconds after the time passed to isoDate. The component rounds this down and renders "10 minutes ago". Since this is the first render, the side effect function runs and starts an interval timer that forces a re-render every 30 seconds.
  • 30 seconds later, the value of isoDate is now 10 minutes and 50 seconds ago. The component re-renders as "11 minutes ago". The interval is still 30 seconds, so the side effect does not run this time.
  • Another 30 seconds pass, and isoDate is now 11 minutes and 20 seconds ago. The component renders "11 minutes ago" again and the side effect function does not run.
  • The interval timer continues to run every 30 seconds, updating the rendered text as necessary, and without running the side effect function.
  • After approximately 50 minutes of refreshes every 30 seconds, the component's re-render will render itself as "1 hour ago". The value of the interval variable in this run of the component's render function is going to be 1800, which is equivalent to 30 minutes. In all previous renders, interval was 30, so this time the value has changed. Since this value is a dependency of the side effect function, React calls the cleanup function set during the initial render, which destroys the 30-second timer, and then launches the side effect function a second time. The side effect function now starts a new 30-minute interval timer.
  • From now on the component re-renders every 30 minutes. If the application is left running long enough, eventually the interval will change to half a day, at which point the side effect function will run again to start an updated interval timer.

Figure 5.5 shows how blog posts look after all the improvements.

Figure 5.5: Blog posts rendered with improved styling

Chapter Summary

  • Use environment variables that start with the REACT_APP_ prefix in a .env file to provide configuration values such as the base URL of the back end.
  • Render functions need to run and return quickly to prevent the application from becoming unresponsive.
  • Use state variables to store data that needs to be retrieved asynchronously, or that can change throughout the life of the component.
  • Use side effect functions to perform network or other asynchronous operations that update state variables. React will automatically re-render a component when any of its state variables change.
  • For state variables associated with data loaded from the network, it is often useful to define specific values that indicate that the data is being retrieved, and also that the data retrieval has failed.
  • A dummy write-only state variable can be used to force a component to re-render when none of its inputs have changed.
  • A side effect function can return a cleanup function that React calls as needed to prevent memory and resource leaks.

25 comments

  • #1 Zack said 2022-08-09T22:27:17Z

    I followed the instructions precisely (I think) but when I attempted to run the "docker-compose up" it errors out with "microblog-api-microblog-api-1 | exec ./boot.sh: no such file or directory" on repeat.

    any suggestions?

  • #2 Miguel Grinberg said 2022-08-10T09:51:33Z

    @Zack: Do you have the boot.sh file in your Microblog-API directory? Is it being copied to the Docker image in the Dockerfile? This should all happen, unless you have made modifications to the project after cloning it from GitHub.

  • #3 Zack said 2022-08-10T14:03:38Z

    @Miguel Yes, it's all there, and according to the build log in the terminal it's copying. I tried cloning the repo to a different directory and tried again and encountered the same error. I've used Docker a decent amount and haven't ever had anything like this happen. I tried rebuilding other projects of mine to see if it was an error with Docker and those worked just fine. I assume this is an issue with my computer otherwise you would have run into it before, so I'll keep looking.

    here is the build log if that's helpful:

    $ docker-compose build --no-cache
    [+] Building 12.4s (12/12) FINISHED
     => [internal] load build definition from Dockerfile                                                                                                                                                                                                         0.0s
     => => transferring dockerfile: 297B                                                                                                                                                                                                                         0.0s 
     => [internal] load .dockerignore                                                                                                                                                                                                                            0.0s 
     => => transferring context: 2B                                                                                                                                                                                                                              0.0s 
     => [internal] load metadata for docker.io/library/python:3.10-slim                                                                                                                                                                                          0.9s 
     => [auth] library/python:pull token for registry-1.docker.io                                                                                                                                                                                                0.0s
     => [internal] load build context                                                                                                                                                                                                                            0.0s
     => => transferring context: 58.01kB                                                                                                                                                                                                                         0.0s 
     => CACHED [1/6] FROM docker.io/library/python:3.10-slim@sha256:2124d4f8ccbd537500de16660a876263949ed9a9627cfb6141f418d36f008e9e                                                                                                                             0.0s 
     => [2/6] COPY requirements.txt ./                                                                                                                                                                                                                           0.0s 
     => [3/6] RUN pip install -r requirements.txt                                                                                                                                                                                                               10.7s 
     => [4/6] COPY api api                                                                                                                                                                                                                                       0.0s
     => [5/6] COPY migrations migrations                                                                                                                                                                                                                         0.0s
     => [6/6] COPY microblog.py config.py boot.sh ./                                                                                                                                                                                                             0.1s
     => exporting to image                                                                                                                                                                                                                                       0.5s
     => => exporting layers                                                                                                                                                                                                                                      0.4s
     => => writing image sha...(cut by me)                                                                                                                                                                 0.0s
     => => naming to docker.io/library/microblog-api   
    
  • #4 Zack said 2022-08-10T14:16:23Z

    Update. I edited the Dockerfile and removed the CMD with ENTRYPOINT ["tail", "-f", "/dev/null"] just to keep it running. I then entered the container with docker exec -it microblog-api-microblog-api-1 bash inside of the container I did this:

    root@e2d669a41809:/# ls api boot config.py dev home lib64 microblog.py mnt proc root sbin sys usr bin boot.sh data etc lib media migrations opt requirements.txt run srv tmp var root@3b2e2774f906:/# ./boot.sh bash: ./boot.sh: /bin/sh^M: bad interpreter: No such file or directory root@3b2e2774f906:/# boot.sh bash: boot.sh: command not found

    I'm not an expert with linux so maybe there's something obvious there that I'm missing. However it looks like it's copying over but just not seeing it for some reason?

  • #5 Zack said 2022-08-10T14:44:51Z

    Figured out the issue. It's because I'm running on a windows machine. and the windows line endings were getting in the way on the boot.sh file. To fix this one needs to add "--config core.autocrlf=input" to the end of their git clone cmd to convert the clone to Unix line endings. The full command would be "git clone https://github.com/miguelgrinberg/microblog-api --config core.autocrlf=input"

    Since no one has encountered this before I guess I'm the only weirdo who develops on a windows machine.

  • #6 Miguel Grinberg said 2022-08-10T15:56:01Z

    @Zack: Nice debugging work, this is actually super useful. I'm guessing you are doing this from a Microsoft Windows computer? The boot.sh file has Windows-style CRLF line endings (note the ^M in the error message). It should have UNIX-style LF line endings to work properly inside the container.

    The command to disable line ending conversion in git is: git config --global core.autocrlf false. If you run this, then clone the repository again, you should have the UNIX line-endings for all files. I need to see if there is a way to configure this option in the repository, so that it overrides your local settings. If that is possible, I'll add it to the repo so that a local configuration fix isn't necessary.

  • #7 Zack said 2022-08-10T17:50:46Z

    Happy to help!

    I have to say this feels pretty awesome. I've followed your guides for a while, your Flask Mega-tutorial basically got me into web-dev. When anyone asks me for help with Flask I point them toward you. Contributing in a small way while following this to round out my Full-stack knowledge is very gratifying.

  • #8 Bayer said 2022-09-12T07:49:36Z

    Hello!

    I was getting the following error when running docker-compose up -d

    ERROR: The Compose file './docker-compose.yml' is invalid because: services.microblog-api.volumes contains an invalid type, it should be a string

    Found a fix in this thread: https://stackoverflow.com/questions/43628804/invalid-type-in-docker-compose-volume

    Apparently "Version 3.2 is required for the extended notation:"

    Hope this can help anyone else that runs into this error.

  • #9 Luke said 2022-10-14T17:55:49Z

    Hello,

    Great tutorial except places where you put "// <-- no changes to blog post JSX" - This makes it really hard to find what I missed. I've spent more time trying to figure out how my Posts.js should look in this chapter than I have reading the whole book.

  • #10 Miguel Grinberg said 2022-10-14T22:19:36Z

    @Luke: I don't understand why this is a problem, can you provide more details? The parts that are suppressed in these listings are the parts that have not changed. If you want to see a detail of the exact changes, then I provide the GitHub diff reports for all chapters (details in the book's Preface).

  • #11 Ben said 2022-11-14T02:55:48Z

    Thanks for putting this together, it's very helpful.

    I haven't yet been able to get the fetch to work due to a CORS error. I believe I followed each step in the tutorial and I set up the flask server by running flask run. I can view the documentation site for the flask server. I think I've found a way around this but haven't tested it yet and just putting feedback here in case you wanted to improve the tutorial.

  • #12 Miguel Grinberg said 2022-11-14T11:36:07Z

    @Ben: You need to show me the output of your server, and the error(s) that you get in the client. Saying that you get a "CORS error" is too vague, as there are many types of errors related to CORS.

  • #13 Peter said 2022-11-15T05:08:22Z

    I'm having the same or similar problem to @Ben. Here is what is being returned at the browser console:

    Access to fetch at 'http://localhost:5000/api/feed' from origin 'http://127.0.0.1:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. Posts.js:11 GET http://localhost:5000/api/feed net::ERR_FAILED 403 (anonymous) @ Posts.js:11 (anonymous) @ Posts.js:19 commitHookEffectListMount @ react-dom.development.js:23150 commitPassiveMountOnFiber @ react-dom.development.js:24926 commitPassiveMountEffects_complete @ react-dom.development.js:24891 commitPassiveMountEffects_begin @ react-dom.development.js:24878 commitPassiveMountEffects @ react-dom.development.js:24866 flushPassiveEffectsImpl @ react-dom.development.js:27039 flushPassiveEffects @ react-dom.development.js:26984 (anonymous) @ react-dom.development.js:26769 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 Posts.js:11 Uncaught (in promise) TypeError: Failed to fetch at Posts.js:11:1 at Posts.js:19:1 at commitHookEffectListMount (react-dom.development.js:23150:1) at commitPassiveMountOnFiber (react-dom.development.js:24926:1) at commitPassiveMountEffects_complete (react-dom.development.js:24891:1) at commitPassiveMountEffects_begin (react-dom.development.js:24878:1) at commitPassiveMountEffects (react-dom.development.js:24866:1) at flushPassiveEffectsImpl (react-dom.development.js:27039:1) at flushPassiveEffects (react-dom.development.js:26984:1) at react-dom.development.js:26769:1

  • #14 Ben said 2022-11-15T08:54:00Z

    Thanks Miguel, I’ll paste any errors here in the future. I left my computer and restarted the python and js processes when I returned and the error disappeared. I can’t reproduce it anymore.

    Appreciate the help and prompt reply regardless.

  • #15 Miguel Grinberg said 2022-11-15T10:35:22Z

    @Peter: Your request is returning a 403 status code. This is strange, because the /api/feed endpoint does not include 403 as one of the possible responses. Are you running Microblog-API in its original form, or have you made modifications? Can you provide the server log as well, please?

  • #16 Peter said 2022-11-16T00:59:59Z

    The Microblog-API is coming straight pout of the box ie.git clone https://github.com/miguelgrinberg/microblog-api.

    there is nothing in the log. doing a curl to the api generates "127.0.0.1 - - [16/Nov/2022 11:54:09] "GET /apispec.json HTTP/1.1" 200 -"

    the browser console gives: Access to fetch at 'http://localhost:5000/api/feed' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. Posts.js:11 GET http://localhost:5000/api/feed net::ERR_FAILED 403 (anonymous) @ Posts.js:11 (anonymous) @ Posts.js:19 commitHookEffectListMount @ react-dom.development.js:23150 commitPassiveMountOnFiber @ react-dom.development.js:24926 commitPassiveMountEffects_complete @ react-dom.development.js:24891 commitPassiveMountEffects_begin @ react-dom.development.js:24878 commitPassiveMountEffects @ react-dom.development.js:24866 flushPassiveEffectsImpl @ react-dom.development.js:27039 flushPassiveEffects @ react-dom.development.js:26984 (anonymous) @ react-dom.development.js:26769 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 localhost/:1 Access to fetch at 'http://localhost:5000/api/feed' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. Posts.js:11 GET http://localhost:5000/api/feed net::ERR_FAILED 403 (anonymous) @ Posts.js:11 (anonymous) @ Posts.js:19 commitHookEffectListMount @ react-dom.development.js:23150 invokePassiveEffectMountInDEV @ react-dom.development.js:25154 invokeEffectsInDev @ react-dom.development.js:27351 commitDoubleInvokeEffectsInDEV @ react-dom.development.js:27330 flushPassiveEffectsImpl @ react-dom.development.js:27056 flushPassiveEffects @ react-dom.development.js:26984 (anonymous) @ react-dom.development.js:26769 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 Posts.js:11 Uncaught (in promise) TypeError: Failed to fetch at Posts.js:11:1 at Posts.js:19:1 at commitHookEffectListMount (react-dom.development.js:23150:1) at commitPassiveMountOnFiber (react-dom.development.js:24926:1) at commitPassiveMountEffects_complete (react-dom.development.js:24891:1) at commitPassiveMountEffects_begin (react-dom.development.js:24878:1) at commitPassiveMountEffects (react-dom.development.js:24866:1) at flushPassiveEffectsImpl (react-dom.development.js:27039:1) at flushPassiveEffects (react-dom.development.js:26984:1) at react-dom.development.js:26769:1 (anonymous) @ Posts.js:11 (anonymous) @ Posts.js:19 commitHookEffectListMount @ react-dom.development.js:23150 commitPassiveMountOnFiber @ react-dom.development.js:24926 commitPassiveMountEffects_complete @ react-dom.development.js:24891 commitPassiveMountEffects_begin @ react-dom.development.js:24878 commitPassiveMountEffects @ react-dom.development.js:24866 flushPassiveEffectsImpl @ react-dom.development.js:27039 flushPassiveEffects @ react-dom.development.js:26984 (anonymous) @ react-dom.development.js:26769 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 await in performWorkUntilDeadline (async) (anonymous) @ Posts.js:19 commitHookEffectListMount @ react-dom.development.js:23150 commitPassiveMountOnFiber @ react-dom.development.js:24926 commitPassiveMountEffects_complete @ react-dom.development.js:24891 commitPassiveMountEffects_begin @ react-dom.development.js:24878 commitPassiveMountEffects @ react-dom.development.js:24866 flushPassiveEffectsImpl @ react-dom.development.js:27039 flushPassiveEffects @ react-dom.development.js:26984 (anonymous) @ react-dom.development.js:26769 workLoop @ scheduler.development.js:266 flushWork @ scheduler.development.js:239 performWorkUntilDeadline @ scheduler.development.js:533 Posts.js:11 Uncaught (in promise) TypeError: Failed to fetch at Posts.js:11:1 at Posts.js:19:1 at commitHookEffectListMount (react-dom.development.js:23150:1) at invokePassiveEffectMountInDEV (react-dom.development.js:25154:1) at invokeEffectsInDev (react-dom.development.js:27351:1) at commitDoubleInvokeEffectsInDEV (react-dom.development.js:27330:1) at flushPassiveEffectsImpl (react-dom.development.js:27056:1) at flushPassiveEffects (react-dom.development.js:26984:1) at react-dom.development.js:26769:1 at workLoop (scheduler.development.js:266:1)

    I will roll the project back to Chapter 4 and try it again.

  • #17 Miguel Grinberg said 2022-11-16T10:44:16Z

    @Peter: Okay, so let's look at this in detail, because what you are saying does not agree with what you are showing me in the logs. Specifically the "there is nothing in the log" part is not possible. The error message shows that the client sent a request to /api/feed, and that this request was responded with a 403 status code. If this does not appear in the server log, then that means that the request is not being handled by Microblog API, but instead by some other server that you may have running on your computer.

    Are you doing this on macOS by any chance? Have you read the Troubleshooting section in the Microblog API readme file? https://github.com/miguelgrinberg/microblog-api#troubleshooting

  • #18 Peter said 2022-11-16T23:27:40Z

    Curious! I changed localhost:5000 to 127.0.0.1:5000 in the .env file and everything works as expected. Yes, I am running MacOS (ventura) and port 500 works fine but I will change it to 4000 just in case. and previously, nothing was appearing in the logs( I was carefully watching them).

  • #19 Ben said 2022-11-17T11:19:54Z

    I had the same experience as Peter. I had the issue re-occur when I re-cloned the project from a later lesson.

    This is the error from the js console. From the python process, there are no logs other than what appears when you start the flask server.

    Access to fetch at 'http://localhost:5000/api/me' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. MicroblogApiClient.js:34 GET http://localhost:5000/api/me net::ERR_FAILED

    I then changed the variable value to http://127.0.0.1:5000, saved the file and ran npm start and I don't get this CORS error.

  • #20 Miguel Grinberg said 2022-11-17T11:24:22Z

    @Ben: Also on macOS? As I referenced above, port 5000 might be in use, so you should follow one of the options indicated in the readme file of the project. Switching to an IP address is a workaournd that may or may not continue working in the future, depending on what Apple decides to do with their Airplay service. Best to change to another port number and avoid any risks in the future.

  • #21 Ben said 2022-11-17T12:04:38Z

    Yes I am on Mac and your suggestion resolved the issue. I changed the port to 4000 for the flask app by running

    flask run --port 4000

    and added MICROBLOG_API_PORT=4000 to the .env file in the flask project. I then edited the .env file in the react project to be:

    REACT_APP_BASE_API_URL=http://localhost:4000

    Thanks Miguel. This course is amazing. I usually don't subscribe for online courses, but this will be the exception.

  • #22 Jeremy said 2022-11-18T18:18:36Z

    Hey Miguel, I love this tutorial! Its been fantastic, though I just hit my first snag. In playing with the first endpoint (/api/feed) I'm getting an unauthorized 401 error, so naturally no data is being returned to the app. I'm using the Flask version of the Microblog API on port 5000, and even in the docs, when clicking the "Send API Request" button, I get the same error. Any idea what could be preventing? Your tutorial mentioned a checkbox for Auth token, but I didn't see any place for that to enable/disable.

    flask console --> 127.0.0.1 - - [18/Nov/2022 11:46:46] "GET /apispec.json HTTP/1.1" 200 - 127.0.0.1 - - [18/Nov/2022 11:58:53] "GET /api/feed HTTP/1.1" 401 - 127.0.0.1 - - [18/Nov/2022 11:58:53] "GET /api/feed HTTP/1.1" 401 - 127.0.0.1 - - [18/Nov/2022 11:59:05] "GET /api/feed HTTP/1.1" 401 - 127.0.0.1 - - [18/Nov/2022 11:59:05] "GET /api/feed HTTP/1.1" 401 -

    app response in browser --> { "code": 401, "description": "The server could not verify that you are authorized to access the URL requested. You either supplied the wrong credentials (e.g. a bad password), or your browser doesn't understand how to supply the credentials required.", "message": "Unauthorized" }

  • #23 Miguel Grinberg said 2022-11-18T18:45:06Z

    @Jeremy: the installation instructions for Microblog API in this chapter instruct you to disable authentication initially. Have you done that? Authentication is a complex topic that is covered later in the tutorial, for now you have to use the API with authentication disabled.

  • #24 Jeremy said 2022-11-18T22:13:09Z

    My apologies, I see that step above in the online text. I was reading thru the book which didnt have the step (yeah I prefer the book to refer back to later ;) ). I also have your Flask Web App book, too. Got it working!

    Thanks again for the quick response and great tutorial!

  • #25 Miguel Grinberg said 2022-11-19T00:00:35Z

    @Jeremy: Glad you got this to work, but note that the book does have this. The text here and in the book is the same, save for minor fixes that I may have applied after you bought your copy.

Leave a Comment