The React Mega-Tutorial, Chapter 6: Building an API Client

Posted by
on under

In Chapter 5, the Posts component was modified to make an API call that gets data from the server. The way it was done, however, does not scale very well, because as the application continues to grow, there's going to be other components that will also need to make their API calls, and having to repeat the API calling logic in several places will be difficult to manage, especially considering that these calls are going to become more complex once authentication and pagination are implemented.

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:

In this chapter you are going to learn how to provide API access to any component that needs it, without having to duplicate code. As part of this effort, you will learn how to use React contexts and how to implement custom hook functions.

What is an API Client?

Having each component call fetch() directly when it needs to make an API call is far from ideal, because it leads to code duplication. A better solution is to have all the logic that deals with making API requests in a single place. For this project the code that makes API calls is going to be encapsulated in a JavaScript class.

Consider the following way in which a client class for this API might be used:

const api = new MicroblogApiClient();
const response = await api.get('/feed');
if (response.ok) {
  setPosts(response.body.data);
}
else {
  setPosts(null);
}

The MicroblogApiClient class will encapsulate all the knowledge about the API, including:

  • The domain and port where the server is deployed
  • The common portion of the path for all API endpoints (/api)
  • How to pass arguments in the query string (which will be needed for pagination)
  • How to catch and handle errors and exceptions
  • How to parse the JSON in the response
  • How to authenticate

A Simple Client Class for Microblog API

The listing below shows a first implementation of a Microblog API client class. Put this code in a file named src/MicroblogApiClient.js.

src/MicroblogApiClient.js: An API client class

const BASE_API_URL = process.env.REACT_APP_BASE_API_URL;

export default class MicroblogApiClient {
  constructor() {
    this.base_url =  BASE_API_URL + '/api';
  }

  async request(options) {
    let query = new URLSearchParams(options.query || {}).toString();
    if (query !== '') {
      query = '?' + query;
    }

    let response;
    try {
      response = await fetch(this.base_url + options.url + query, {
        method: options.method,
        headers: {
          'Content-Type': 'application/json',
          ...options.headers,
        },
        body: options.body ? JSON.stringify(options.body) : null,
      });
    }
    catch (error) {
      response = {
        ok: false,
        status: 500,
        json: async () => { return {
          code: 500,
          message: 'The server is unresponsive',
          description: error.toString(),
        }; }
      };
    }

    return {
      ok: response.ok,
      status: response.status,
      body: response.status !== 204 ? await response.json() : null
    };
  }

  async get(url, query, options) {
    return this.request({method: 'GET', url, query, ...options});
  }

  async post(url, body, options) {
    return this.request({method: 'POST', url, body, ...options});
  }

  async put(url, body, options) {
    return this.request({method: 'PUT', url, body, ...options});
  }

  async delete(url, options) {
    return this.request({method: 'DELETE', url, ...options});
  }
}

The class constructor stores the common part of the URL that is used for all endpoints in the base_url attribute. This includes the base URL obtained from the REACT_APP_BASE_API_URL environment variable, and the /api path prefix.

The actual request making logic, which is a slightly more elaborate version of the fetch() code used in the Posts component, is in the request() method. To make this class easier to use, there are shortcut methods for get(), post(), put() and delete() requests that invoke request() with appropriate settings.

Let's review the request() method in detail. This method takes all of its arguments from an options object. The method and url keys in this object are set by the get(), post(), put() and delete() helper methods, from its input arguments. The body key is set by post() and put(). Any additional options that the caller might need, such as custom headers or query string parameters, are accepted as a last argument on the four helper methods, and passed through.

The request() method starts by looking for the query key in the options, which allows the caller to specify query string arguments as an object, for convenience and readability. The URLSearchParams class available in the browser is used to render the object in the proper query string format.

When the server is down or unresponsive, the fetch() call raises an exception. For that reason, the fetch() call is made inside a try/catch block that handles this condition. This application handles fetch errors in the same way as if the server had returned a response with a 500 status code. The catch block builds a response object similar to that of fetch, but preloaded with the 500 status code response. The body in this error response is formatted in the same style as actual API errors returned by the Microblog API service.

The actual fetch() call is similar to the one in Posts, but generalized to work with the method, url, query, body and headers options that are passed in. Some common options are automatically added in this method. For example, a JSON content type, which is needed for POST and PUT requests that have a body, is automatically added. Also, the body argument is automatically rendered as a JSON object so that the caller doesn't have to worry about this detail.

The ok, status and body keys returned in the fetch() response are used to generate a simpler response object for the caller that has the JSON payload already decoded to an object. Responses from this API client will have three attributes:

  • ok: a value of true or false that indicates the success or failure of the request.
  • status: the numeric HTTP status code of the server response.
  • body: an object with the payload returned in the body of the response, when available.

Sharing the API Client through a Context

The first way one may consider incorporating the MicroblogApiClient class into the application is to create an instance of the class in each component that needs API access. The problem with this approach is that it is inefficient when many components need to make API calls.

A better solution is to create a single instance that is shared among all the components that need to use the API. In Chapter 3 you learned that components can share data by passing props, but doing this would add a lot of boilerplate code as the API client instance would have to be passed down from high-level components down to low-level components through all the levels in between.

For cases when something needs to be shared with many components in different levels of the tree, React provides contexts.

A React context is created with the createContext() function from the React library:

import { createContext } from 'react';
const MyDataContext = createContext();

Once the context is created, it has to be inserted in the component hierarchy, high enough so that it is a parent to all the components that will use data shared through it. Here is an example of how this is done:

export default function MyApp() {
  return (
    <Container>
      <Header />
      <MyDataContext.Provider value={'data-to-share'}>
        <Sidebar />
        <Content />
      </MyDataContext.Provider>
    </Container>
  );
}

Here the MyApp component inserts a context into its JSX tree. This is done by using the Provider attribute of the context object that was created with the createContext() function.

In the above example, the Sidebar and Content components, along with all of its children, will be able to gain access to the value prop set in the context provider component. The Header component will not be able to use the context, because it is not a child of the context provider element.

To access the value of a context, the child component can use the useContext hook as follows:

import { useContext } from 'react';
import { MyDataContext } from './MyDataContext';

export default function Sidebar() {
  const myData = useContext(MyDataContext);
  // ...
}

However, in practice, this leads to code that is not very clear to read, because the components that want to use the context need to import this strange context object only to be able to pass it to the useContext() hook.

An approach that is preferable is to create a custom hook function that returns the encapsulates the useContext() call. A custom hook is a function that starts with the word use. As previously discussed, hook functions are considered special by React. In particular, they are the only functions outside of component render functions that can call other hooks. Here is an example custom hook for the above context:

import { useContext } from 'react';

export function useMyData() {
  return useContext(MyDataContext);
}

The custom hook can be added in the same source file as the context object. Child components of the context then only need to import the hook function to access the context, leading to code that is more readable.

import { useMyData } from './MyDataContext';

export default function Sidebar() {
  const myData = useMyData();
  // ...
}

Ready to implement a context and a custom hook for the Microblog API client? Begin by creating a src/contexts subdirectory, where all the contexts of this application will be stored:

mkdir src/contexts

Add an ApiProvider component in src/contexts/ApiProvider.js that implements an API context, along with a useApi() custom hook.

src/contexts/ApiProvider.js: An API context

import { createContext, useContext } from 'react';
import MicroblogApiClient from '../MicroblogApiClient';

const ApiContext = createContext();

export default function ApiProvider({ children }) {
  const api = new MicroblogApiClient();

  return (
    <ApiContext.Provider value={api}>
      {children}
    </ApiContext.Provider>
  );
}

export function useApi() {
  return useContext(ApiContext);
}

First, notice how this source file has two exported functions. The ApiProvider component function is exported as the default symbol for the module, as with all other React components, but now there is also the custom hook useApi(), which has to be exported so that other components can use it.

The component renders the context's ApiContext.Provider, and puts its own children inside it. This effectively enables all the child components to access the context.

It is safe to assume that many of the application's components will need API access, so it makes sense to add this context high in the component hierarchy. Below you can see how it is added in the App component.

src/App.js: Add the API context to the application

import Container from 'react-bootstrap/Container';
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import ApiProvider from './contexts/ApiProvider';
import Header from './components/Header';
import FeedPage from './pages/FeedPage';
import ExplorePage from './pages/ExplorePage';
import UserPage from './pages/UserPage';
import LoginPage from './pages/LoginPage';

export default function App() {
  return (
    <Container fluid className="App">
      <BrowserRouter>
        <ApiProvider>
          <Header />
          <Routes>
            <Route path="/" element={<FeedPage />} />
            <Route path="/explore" element={<ExplorePage />} />
            <Route path="/user/:username" element={<UserPage />} />
            <Route path="/login" element={<LoginPage />} />
            <Route path="*" element={<Navigate to="/" />} />
          </Routes>
        </ApiProvider>
      </BrowserRouter>
    </Container>
  );
}

The context is added as the child of the BrowserRouter component from React-Router, with all the remaining components as its children. Since all the application components are children of this context, they are all able to use the useApi() hook to obtain access to the API client when they need to.

Now the Posts component can take advantage of the new hook to make its API call.

src/components/Posts.js: Using the useApi() hook

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

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

  useEffect(() => {
    (async () => {
      const response = await api.get('/feed');
      if (response.ok) {
        setPosts(response.body.data);
      }
      else {
        setPosts(null);
      }
    })();
  }, [api]);

  ... // <-- no changes in the rest of the function
}

Thanks to the useApi() hook function, the code in this component is now very clear, but there are a couple of interesting changes to pay attention to. First, note how the useApi() function is imported. Since this function was declared as a non-default export in ApiProvider.js, it has to be imported using a destructuring assignment.

The second argument to the useEffect() hook used to be an empty array, but is now [api]. As you already learned, this array is used to tell React what are the dependencies of the side effect function. With an empty array, the function only ran during the first render of the component. When dependencies are given, a change in a dependency will make React call the side effect function again, so that the component is always up-to-date.

The React build process analyzes your code, and one of the things it looks for is possible dependency omissions. If you forget to include api in the dependency array of the side effect function, the build generates a warning to alert you that this variable is used inside the function, which is a strong indicator that it should be treated as a dependency.

The User Profile Page

The Post component renders the author of each blog post as a link that points to the /user/{username} page, defined within the React application. A placeholder for this page is implemented in the UserPage component. In this section this component will be enhanced to request user information from the API and render it to the page.

In Microblog API, you can get information about a user by sending a request to /api/users/{username}. If you want to see what is the response for this endpoint, consult the API documentation by going to the root URL of your API service (for example, http://localhost:5000) and looking up the "Retrieve a user by username" endpoint.

The approach to construct the profile page is largely similar to that of the post feed, and it involves the following:

  • Create a state variable for the data that needs to be requested from the API
  • Obtain the API client object with the useApi() hook
  • Define a side effect function that makes the API request and updates the state variable when the data is received
  • Render a spinner while the state variable on the first render, then show the data from the state variable on the re-render

You can see the complete implementation of UserPage below.

src/pages/UserPage.js: User profile page

import { useState, useEffect } from 'react';
import Stack from 'react-bootstrap/Stack';
import Image from 'react-bootstrap/Image';
import Spinner from 'react-bootstrap/Spinner';
import { useParams } from 'react-router-dom';
import Body from '../components/Body';
import TimeAgo from '../components/TimeAgo';
import { useApi } from '../contexts/ApiProvider';

export default function UserPage() {
  const { username } = useParams();
  const [user, setUser] = useState();
  const api = useApi();

  useEffect(() => {
    (async () => {
      const response = await api.get('/users/' + username);
      setUser(response.ok ? response.body : null);
    })();
  }, [username, api]);

  return (
    <Body sidebar>
      {user === undefined ?
        <Spinner animation="border" />
      :
        <>
          {user === null ?
            <p>User not found.</p>
          :
            <Stack direction="horizontal" gap={4}>
              <Image src={user.avatar_url + '&s=128'} roundedCircle />
              <div>
                <h1>{user.username}</h1>
                {user.about_me && <h5>{user.about_me}</h5>}
                <p>
                  Member since: <TimeAgo isoDate={user.first_seen} />
                  <br />
                  Last seen: <TimeAgo isoDate={user.last_seen} />
                </p>
              </div>
            </Stack>
          }
        </>
      }
    </Body>
  );
}

As in the original placeholder page, the user to render is obtained from the URL, using the useParams() hook from React-Router. The user state variable is then defined to hold the user information, and the useApi() hook is used to gain access to the API client instance.

The side effect function makes the API request, and calls setUser() to update the state variable with the response. As in the Posts component, the state variable starts with an undefined value, and once the request returns it is updated to the user information when the request succeeds, or to null if the request fails.

If you leave the dependency array for the side effect function empty, the React build warns that both api and username are dependencies, so both should be included, so that the component updates when they change. Making the username variable a dependency is extremely important, as it would cause the user information to be refreshed whenever the user changes.

The render code uses the Body component and includes the sidebar. Inside the body, if the user state variable is undefined a spinner is rendered to indicate that the data is being retrieved.

If the user state variable is null, which indicates a request error, an empty content area is shown. Recall that a centralized error handling solution for API errors will be built later.

When the user state variable is set to the user's information, a page for the user is built using many of the user attributes included in the response.

To create a nice layout for the profile page, a horizontal Stack component is used to create two sections for a large avatar (configured with a size of 128x128 pixels) on the left, and the username, user description and timestamps on the right. The TimeAgo component used for the blog post timestamps is also used here for the member_since and last_seen attributes.

With these changes, you can click on any username in the feed page to access their profile page. This is shown in Figure 6.1.

Figure 6.1: User profile page

Making Components Reusable Through Props

One small detail that would make the user profile page much more interesting is to include a list of the user's blog posts below the user details. This would be similar to the list rendered in the feed page, but restricted to only show the posts from the user being viewed. And the Explore page, which is still a placeholder, also needs to render a list of posts, this time all the posts in the system. So there are already three pages in the application that need to render lists of posts.

The Posts component already knows how to list a collection of blog posts, and it does it nicely, but it only renders the user's feed. To avoid building two nearly identical components that render the posts of a user in the user page, and all the posts in the explore page, the Posts component can be extended to render different lists of posts, depending on what the parent component wants.

To make this component more flexible, a content prop can be added to it. If this prop is not given, or if it is set to the string feed, then the user's feed is displayed as before. If the prop is set to explore, then all the available blog posts are rendered. Finally, if the content prop is set to any other value, it is assumed to be a user ID, and then the posts that are displayed are those of the user with the requested ID.

See below the changes to the Posts component to implement this.

src/components/Posts.js: Configure post content to display

... // <-- no changes to imports

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

  let url;
  switch (content) {
    case 'feed':
    case undefined:
      url = '/feed';
      break;
    case 'explore':
      url = '/posts';
      break
    default:
      url = `/users/${content}/posts`;
      break;
  }

  useEffect(() => {
    (async () => {
      const response = await api.get(url);
      if (response.ok) {
        setPosts(response.body.data);
      }
      else {
        setPosts(null);
      }
    })();
  }, [api, url]);

  ... // <-- no changes in the rest of the function
}

In the previous version of this component, the URL for the API request was hardcoded to /feed. Now a switch statement is used to determine what URL to request based on the value of the content prop:

  • for feed, or if content prop is not defined, the /feed URL is requested as before
  • for explore, the /posts URL is requested
  • for any other value, string interpolation is used to request the /users/{content}/posts URL

All these URLs return lists of blog posts, and they all use the same response format. But each URL returns a different list. Remember that you can review the documentation for the endpoints in the documentation site provided by Microblog API.

The dependencies of the side effect function have been expanded one more time. Now url is also in the list, because it is a variable from outside that is used in the effect function, and consequently, whenever it changes the effect needs to run again to refresh the list of posts and keep it consistent with the state of the component.

The user profile page now can include a list of posts by the user being viewed. The changes to UserPage are below.

src/pages/UserPage.js: Show user's blog posts in profile page

... // <-- no changes to existing imports
import Posts from '../components/Posts';

export default function UserPage() {
  ... // <-- no changes in the main logic of the function

  return (
    <Body sidebar>
      {user === undefined ?
        <Spinner animation="border" />
      :
        <>
          {user === null ?
            <p>Could not retrieve blog posts.</p>
          :
            <>
              <Stack direction="horizontal" gap={4}>
                ... // <-- no changes to user details
              </Stack>
              <Posts content={user.id} />
            </>
          }
        </>
      }
    </Body>
  );
}

This change just wraps the Stack component in <> and </>, so that a Posts component can be added after it, while keeping a single parent in the JSX tree. The Posts component in this case has the content prop set to user.id, to request that only the posts from the user in question are rendered.

The updated user profile page is shown in Figure 6.2.

Figure 6.2: User profile page with blog posts

The Posts component is now also able to render posts in the Explore page, when the content prop is set to explore. The ExplorePage placeholder can be updated to display real content, as shown below.

src/pages/ExplorePage.js: the Explore page

import Body from '../components/Body';
import Posts from '../components/Posts';

export default function ExplorePage() {
  return (
    <Body sidebar>
      <Posts content="explore" />
    </Body>
  );
}

This is the perfect example of how good component reuse techniques help reduce code complexity. With just three lines of JSX code, a brand-new page was added to the application!

Pagination

The lists of blog posts returned by Microblog API are paginated, which means that for a given request, the server returns the first few items, up to a configurable maximum (25 with default settings).

You've seen that the data attribute in the response payload contains the list of requested items. The server includes a second attribute in all the responses that include lists called pagination, which provides details about the portion of the complete list that was returned. Below you can see an example JSON payload, including pagination details:

{
  "data": [

  ],
  "pagination": {
    "count": 25,
    "limit": 25,
    "offset": 0,
    "total": 124
  }
}

Here data includes an array of 25 elements. This is reported in the pagination attribute under count. The limit key indicates what is the maximum page size that can be returned. The offset key reports the zero-based index of the first element returned, and total reports the size of the entire list.

Using these pagination details and a bit of math, it is possible to determine if there are more elements in the list following the ones that were returned. For the example above, the index of the last returned element is offset + count - 1, or 24. Since total is 124, the index of the last element of the list is 123, indicating that the user should be given the option to retrieve more elements if desired.

To let the user add for more elements, a "More" button is going to be displayed at the bottom of the list, when there are more elements available. Clicking the button will trigger a new request to be issued for more items.

Let's begin by implementing a generic More component, which displays a button to load more items when appropriate.

src/components/More.js: A pagination button

import Button from 'react-bootstrap/Button';

export default function More({ pagination, loadNextPage }) {
  let thereAreMore = false;
  if (pagination) {
    const { offset, count, total } = pagination;
    thereAreMore = offset + count < total;
  }

  return (
    <div className="More">
      {thereAreMore &&
        <Button variant="outline-primary" onClick={loadNextPage}>
          More &raquo;
        </Button>
      }
    </div>
  );
}

The More component has two props. The pagination prop receives a pagination object extracted from a Microblog API response, including the four attributes discussed above. The loadNextPage prop is a handler that the parent component must provide, used to load the next page of items when the More button is clicked. Requiring the parent component to provide the logic to load the next page allows this component to be fully generic, instead of being tied to a specific part of the application.

The thereAreMore local variable uses the keys in the pagination prop to determine if there are more items to retrieve, and in that case, it displays the button. When thereAreMore has a false value, the button is not displayed, since there wouldn't be any items left to retrieve from the server.

The button is wrapped in a <div> element that is assigned the More class. This class can be added to index.css to provide minor styling details to the component.

src/index.css: Pagination button styling

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

.More {
  margin-top: 10px;
  margin-bottom: 10px;
  text-align: right;
}

To support pagination in all the pages that show lists of posts, the Posts component can be expanded to use the More button, as shown below.

src/components/Posts.js: Post pagination

... // <-- no changes to existing imports
import More from './More';

export default function Posts({ content = 'feed' }) {
  const [posts, setPosts] = useState();
  const [pagination, setPagination] = useState();
  const api = useApi();

  ... // <-- no changes to how url is determined

  useEffect(() => {
    (async () => {
      const response = await api.get(url);
      if (response.ok) {
        setPosts(response.body.data);
        setPagination(response.body.pagination);
      }
      else {
        setPosts(null);
      }
    })();
  }, [api, url]);

  const loadNextPage = async () => {
    // TODO
  };

  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} />)
              }
              <More pagination={pagination} loadNextPage={loadNextPage} />
            </>
          }
        </>
      }
    </>
  );
}

There are a few changes in this component:

  • The More component is imported at the top.
  • A new pagination state variable is included.
  • When the response to the server request is received, the posts state variable is assigned the data, and the pagination state variable is assigned the pagination details.
  • A loadNextPage function is defined as an inner function in the component, for now as a placeholder.
  • The More component is added right after the list of blog posts is rendered. The pagination state variable and the loadNextPage function are passed as props.

With these changes, you should see the More button after the blog posts. If you don't see it, then the list of posts that you are looking at does not have additional data. With the initial randomized data that is initialized with Microblog API, the Explore page should have enough posts to require pagination, but the Feed page may not. Figure 6.3 shows how the pagination button looks like.

Figure 6.3: Pagination button

What's left to do now is to implement the logic in the loadNextPage() function. If you review the Microblog API documentation for any of the requests that return a list of blog posts, you'll notice that they all accept three optional query parameters, limit, offset and after.

The limit option is used to change the size of the returned page. This application does not need to do this, the client is happy to accept the default page size used by the server, so this option does not need to be used in this pagination implementation.

The offset option can be used to request items starting at a specific offset. For example, if the feed page is displaying the first 25 blog posts, passing offset=25 to the same request URL would return blog posts 25 to 49 of that same list.

The after option can also be used to request additional items, but it works differently than offset. For after, the argument needs to be the timestamp value of the last post being displayed, so for example, it could be after=posts[posts.length - 1].timestamp. The server then returns items starting right after the provided time.

Which of offset and after is better? This really depends on each particular case. One disadvantage of offset is that it only works well for lists that have new items added at the end, because items added in any other position would alter the indexes assigned to existing elements. The blog posts returned by Microblog API are sorted by their publication date, but in descending order, so new posts are always inserted at the start of the list, pushing older items down. On the other side, after works well only for lists that have a well-defined ordering.

Below you can see the implementation of the loadNextPage() function for the Posts component, using the after query parameter.

src/components/Posts.js: Load the next page of posts

  const loadNextPage = async () => {
    const response = await api.get(url, {
      after: posts[posts.length - 1].timestamp
    });
    if (response.ok) {
      setPosts([...posts, ...response.body.data]);
      setPagination(response.body.pagination);
    }
  };

The function makes a request on the same url as before, but this time the second argument for the api.get() call is added with the after query parameter.

When the response is received, the posts state variable is updated to a combined list of old and new blog posts, using the ... spread operator. The pagination state variable is also updated with the new details received in the paginated request. The change to these two state variables will cause the Posts component to render again, which in turn will make the new blog posts appear at the bottom of the list.

After the new posts are displayed, the More component will remain at the bottom, so the updated pagination state will be used again to determine if there are even more items waiting in the server, and in that case the button will continue to be visible. After the last batch of items are retrieved, the More component will not display the button anymore.

Chapter Summary

  • Write the back end calling logic in a single place, for example in an API client class.
  • To share a data item with a subcomponent, pass it down with a prop. To share a data item with many subcomponents across several levels, use a context.
  • Use a custom hook function to make the code that uses a context more readable.
  • Use props to make a component more generic, and facilitate its reuse.
Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

23 comments
  • #1 Russ GIbson said

    When I first followed this, I accidentally used "isodate" instead of "isoDate" in the Member since: and Last seen: lines of UserData.js. This caused the rendering to fail, with a blank page, and all navigation to fail as well (e.g. back button didn't cause page to rerender where it came from).

    What concerns me is that I received no error message or indication of what caused the problem. It wasn't until I created a tmp.js with the contents of UserData.js from above and did a diff between the that and my version that I was able to determine what I screwed up. I thought one of the useful features of react was that it would warn me on stupid bugs like this? Up until this specific issue, that has always been the case. Is this a corner case, or do I misunderstand that feature of react?

  • #2 Miguel Grinberg said

    @Russ: this should have been reported in the browser's console as an error. Did you look there? That's the first place you should look for clues when the application works in a strange way.

  • #3 Kevyn said

    In this example you stated that the Explore page should be able to show the 'More' button, but I do not believe we have done anything with that page besides create it.

    Perhaps I missed something?

    I did update my local to import Posts passing in the explore param for content, I do see post and the button works as intended. I am following along via the book and double checking here so just wanted to make sure I did not miss anything.

    Thanks for writing this as it has been very easy to follow and understand.

  • #4 Miguel Grinberg said

    @Kevyn: At the end of the section titled "Making Components Reusable Through Props" I show the complete implementation of the Explore page. It is just a handful of lines! The page inherits the behavior of the More button from the Posts component.

  • #5 Tomas said

    Hi @Miguel, another quick question if that's ok. I understand passing api as a dependency to useEffect. However I can't think of an instance where the api would 'change' causing the component to re-render. Do you have any examples or scenarios?

    On a similar note, would it be possible to define const api = useApi() inside the useEffect() function?

    Many thanks

  • #6 Miguel Grinberg said

    @Tomas: This is covered later in the tutorial, as it is an advanced topic that would be difficult to understand at this early point.

    Also, it is not possible to use hooks inside side effect functions, they can only be used inside the main body of component functions.

  • #7 Daniel Hernandez said

    Hi Miguel, I completed the Flask Megatutorial and started the React megatutorial. The API is covered as overview in Flask one but your MicroblogApi changed so much and for sure it required other ebook/course too.
    It is more robust and secure for projects in React than customize Flask API section.
    I am the first happily to enroll on it. Please evaluate, it is probably a natural step to apply the knowledge acquiring in React Mega Tutorial.

    Saludos y muchas gracias.

  • #8 Miguel Grinberg said

    @Daniel: You can learn about the back end by studying the source code, which is fully available on GitHub. Maybe I will write a course about it, but don't let that prevent you from learning how it works on your own!

  • #9 JM said

    What a great collection of articles! Thank you very much Miguel.
    I am using my own API and this just works great.

    By default you have the application/json header to be sent on every request with your MicroblogApiClient class, regardless of the type of request. I had to comment this header because otherwise I was getting the following error: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS/Errors/CORSDidNotSucceed. I am using my own API and the responses carry the "Access-Control-Allow-Origin": "*" header. I would like to ask you whether I should be able to make a get request even with the application/json header that I had to comment or not just to check whether I have to tune further my backend.

    Thank you again for your time and effort,

  • #10 Miguel Grinberg said

    @JM: Not 100% sure, but my guess is that you need to add the Content-Type header to your safe list by including it in the Access-Control-Request-Headers. Responding with a * for origins is not ideal, but I think that is not the problem here.

  • #11 Daniel Rocha said

    Hi Miguel! First of all, thank you for this tutorial and specially for your work in Flask. It's great!

    I have a question: should we implement the API client logic with a function instead of a class, would it still be better to use it through context, or would it be fine then to just import the function and use it normally?

  • #12 Miguel Grinberg said

    @Daniel: if your function does not need to hold any state, then you can just import it and use it.

  • #13 George Moore said

    Thanks for all your work on tutorials. It was my goto for Flask and now with React it too is very well done.

    I am stuck with the Client API. Everything works except when I added Posts to the User profile. I get the following error.

    Access to fetch at 'http://127.0.0.1:5000/api/user/$%7Bcontent%7D/posts' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

    Can't track down what could be going wrong.

    Thanks again for all your help

  • #14 Miguel Grinberg said

    @George: Are you using your own back end? The back end must have CORS configured to allow the front end to make API requests. This is configured in the Microblog API project referenced in this tutorial.

  • #15 George Moore said

    No, I pulled your flask backend and ran it locally. Thought i followed the few setup steps but will double-check. Thanks for such a quick response.

  • #16 Miguel Grinberg said

    @George: I just noticed that the URL mentioned in the CORS error is incorrect. When you URL decode it, the URL is http://127.0.0.1:5000/api/user/${content}/posts, which means that variables aren't being replaced, probably because you defined the string as a normal string with quotes, instead of as a template string with back ticks.

  • #17 George Moore said

    That was it. Should have picked it up too. Thanks again for your help!

  • #18 Joe T said

    Thanks for another great tutorial! I have been going through this to help me as I separate a React front end I've built from its Flask backend (based on the structure you presented in your Flask tutorial).

    I've implemented the React application exactly as you have instructed up through this API client chapter. I've noticed that the front end makes each API call twice every time. This is observable in Dev Tools in the browser and the back end trace reflects it as well. This is a concern for my own application as it operates in an environment where outbound data transfer is metered. Have I missed a detail? Any thoughts?

    Thanks in advance!

  • #19 Miguel Grinberg said

    @Joe: The side effect functions run twice when using React 18 in development mode. This does not occur when you use the production built version of the application. This is documented in the official React docs: https://reactjs.org/docs/strict-mode.html#ensuring-reusable-state. If this bothers you, you can downgrade to React 17, which didn't do this. I've also seen some hacks that disable this behavior in React 18, but I frankly don't see a need, since this only happens during development.

  • #20 Joe T said

    @Miguel, thanks again! I am able to see the redundant call go away with a production build. Your expertise is much appreciated as always.

  • #21 marcus kamure said

    can you kindly explain, looking for query string used in the api especially the (options,query || {}) part of it because am a bit lost here

  • #22 Miguel Grinberg said

    @marcus: you can pass query string values in the options object. For example:

    const response = await api.get('/foo', {query: {page: 1, per_page: 10}});
    
  • #23 marcus kamure said

    Thanks Miguel for the effort you put in to produce work that has a great impact on people, i really appreciate that

Leave a Comment