React tutorial blog app: react_router_v6-main, react_axios_requests-main, export default axios.create, scrollTo() method

Note: This post is a details-post for the post: Learning web app development through free online tutorials – Organized Notes and Log, https://raviswdev.blogspot.com/2024/03/learning-web-app-development-through.html .

The blog app in react_router_v6-main folder in the tutorial has 5 state variables whereas in my variant I used only 2 state variables (posts (array) and searchPosts (string)). I passed these two state variables to all the key pages SinglePost, EditPost, NewPost, SharedLayout etc. That may be a poorer OO design as all these components have access to all the key data and can change it. However it simplified the code. Also I implemented the add, update and delete post functions in the components having the UI for them as the components had access to posts and setPosts, wherease in the tutorial code these functions (add and delete as update functionality is not provided at this stage but is provided later) in App.js and passes the functions as props to relevant components. Yet another key difference is that the tutorial code uses useEffect() to set the list of posts to be shown to user (searchResults) and which has dependencies of posts and search (string). I did not use useEffect() and wrote code within the return statement of the App component to filter the posts based on search string value.

In the tutorial, code that adds functionality for edit post is in the axios project: folder name: react_axios_requests-main. In this code, the form for new post and edit post though quite similar is done separately for new post (in NewPost.js) and for edit post (in EditPost.js).

I wrote a variation with a common form component called NewnEditForm which is used by NewPost component and EditPost component. The related code is given below:

NewnEditForm.js

import { useState } from "react";
import { useNavigate } from "react-router-dom";
const NewnEditForm = ({
  postId = null,
  postTitle,
  postBody,
  newPostChosen,
  addOrSavePost,
}) => {
  const [newnEditPostTitle, setNewnEditPostTitle] = useState(postTitle);
  const [newnEditPostBody, setNewnEditPostBody] = useState(postBody);

  const navigate = useNavigate();

  return (
    <form
      className="NewnEditPostForm"
      onSubmit={(e) => {
        e.preventDefault();
        addOrSavePost(newnEditPostTitle, newnEditPostBody, postId);
        navigate("/");
      }}
    >
      <label htmlFor="title">Title: </label>
      <input
        type="text"
        id="title"
        required
        autoFocus
        value={newnEditPostTitle}
        onChange={(e) => setNewnEditPostTitle(e.target.value)}
      />
      <label htmlFor="body">Post Body: </label>
      <textarea
        id="body"
        value={newnEditPostBody}
        onChange={(e) => setNewnEditPostBody(e.target.value)}
      />
      <p>
        {newPostChosen ? (
          <button type="submit">Create Post</button>
        ) : (
          <button type="submit">Save Post</button>
        )}
      </p>
    </form>
  );
};
export default NewnEditForm;
NewPost.js

import NewnEditForm from "./NewnEditForm";

const NewPost = ({ posts, setPosts }) => {
  function addNewPost(postTitle, postBody) {
    const newId = posts.length > 0 ? posts[posts.length - 1].id + 1 : 1;
    const datetime = Date();
    const tmpPosts = [...posts];
    tmpPosts[posts.length] = {
      id: newId,
      datetime: datetime,
      title: postTitle,
      body: postBody,
    };
    setPosts(tmpPosts);
  }

  return (
    <NewnEditForm
      postTitle={""}
      postBody={""}
      newPostChosen={true}
      addOrSavePost={addNewPost}
    />
  );
};
export default NewPost;
EditPost.js

import { useParams } from "react-router-dom";
import NewnEditForm from "./NewnEditForm";
import NotFound from "./NotFound";

const EditPost = ({ posts, setPosts }) => {
  const { id } = useParams();
  const post = posts.find((post) => post.id.toString() === id);

  function savePost(postTitle, postBody, postId) {
    const tmpPosts = posts.map((post) => {
      return post.id === postId
        ? {
            id: post.id,
            datetime: post.datetime,
            title: postTitle,
            body: postBody,
          }
        : post;
    });
    setPosts(tmpPosts);
  }

  return post ? (
    <NewnEditForm
      postId={post.id}
      postTitle={post.title}
      postBody={post.body}
      newPostChosen={false}
      addOrSavePost={savePost}
    />
  ) : (
    <NotFound />
  );
};

export default EditPost;
I think it is the first time in any React program I wrote that I have initialized state variables from props (see NewnEditForm.js). I tried searching the net to see if this is OK but I could not get definitive articles. I think I need to search more. But the code works and so this approach may be OK. The alternative solution which avoids this, is to have different post title and body state variables in NewPost and EditPost components and pass them as props to NewnEditForm.js.

In the tutorial, App.js code defines one set of Title and Body state variables to be passed to NewPost component as props and another set of Title and Body state variables to be passed to EditPost component as props. The NewPost and EditPost components use these state variables (passed as props) for controlled input fields of title and body in their forms.

The tutorial code has a requirement of setting the Title and Body state variables for the passed id (as URL parameter) in the EditPost component. It uses useEffect() to set them. [If it did that in the regular EditPost component code, my understanding is that it would result in an (attempted) infinite re-render of the component and so the program would crash.]

In my variation, in the EditPost component as I use ordinary variables (not state variables) to get the title and body for the passed id (as URL parameter) and pass them to the NewnEditForm component as props, I do not have to use useEffect().

P.S. Note that in my variation I have not spent much effort on CSS and so UI niceties as the focus here is on learning React and not CSS.

...


The tutorial project uses axios.create to create an instance of axios with a base URL. Then while using the instance the part after the base URL alone can be specified as the path.


I had a few questions about how exactly using the import works in this case. After some digging up I got some answers …

The key statement in the above article’s api.js file is:

export default axios.create({
  baseURL: `http://jsonplaceholder.typicode.com/`
});

My understanding is that what is exported is the return value of the axios.create function which has been passed baseURL parameter. It need not be named as it is exported as default.

In the source file that imports api.js as API in the example, the axios functions are invoked, for example, as API.delete with passed url omitting the base URL part. My understanding is that the “import API from ‘../api’;” statement in the other .js file which uses api.js, results in execution of axios.create function and its return value is then API.

The official documentation page: The Axios Instance, https://axios-http.com/docs/instance , explains that axios.create returns an instance and this instance can be used to invoke Axios functions like get and delete.

But what happens when another source file also imports api.js and invokes say API.get, and both source files come into play while rendering some components? Does axios.create get executed again? My understanding now is that axios.create is executed only once.

To get this understanding I wrote some code and tried to confirm my understanding from Internet info. on this. The code and its results are given below:

expFnRtnValue.mjs [.mjs was needed instead of .js as running this code (which has import statements) using node seems to need .mjs extensions]

export default fn();

function fn() {
  console.log("fn invoked");
  return Math.random();
}

useFnRtnValue.mjs

import fnRtnValue from "./expFnRtnValue.mjs";
import useFnRtnValueAgain from "./useFnRtnValueAgain.mjs";

console.log("Inside useFnRtnValue.mjs ... Just before logging fnRtnValue");
console.log(fnRtnValue);
console.log("Inside useFnRtnValue.mjs ... Just before invoking useFnRtnValueAgain()");
useFnRtnValueAgain();

useFnRtnValueAgain.mjs

import fnRtnValue from "./expFnRtnValue.mjs";

export default function useFnRtnValueAgain() {
  console.log("Inside useFnRtnValueAgain function ... Just before logging fnRtnValue");
  console.log(fnRtnValue);
}

Running program using “node .\useFnRtnValue.mjs” produced the following output:

fn invoked
Inside useFnRtnValue.mjs ... Just before logging fnRtnValue
0.38576122467545426
Inside useFnRtnValue.mjs ... Just before invoking useFnRtnValueAgain()
Inside useFnRtnValueAgain function ... Just before logging fnRtnValue
0.38576122467545426

Analyzing the above output, tells us:
1) fn is invoked even before useFnRtnValue’s first console.log statement is executed.
2) fn is invoked only once even though its return value is referred to twice via separate import statements – once in useFnRtnValue.mjs and once in useFnRtnValueAgain.mjs. So it seems that such import statement execution is done only once in a program run and the same return value is provided to multiple files that import it. Note that the random value provided is same in useFnRtnValue.mjs and useFnRtnValueAgain.mjs

Now going back to the api.js code, it is clear that axios.create() function will be invoked only once and the return value of that invocation will be provided to code in multiple source files that import api.js.

I tried to see if the official reference page for import, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import , explains this clearly. It is a long page and in my browse through it I could not find a clear explanation of this. It does have a section on “side-effects”, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import#import_a_module_for_its_side_effects_only but in this case one does not import anything and that is not what we are looking for (as we do import the axios instance returned by axios.create()).

Section “A module code is evaluated only the first time when imported” in https://javascript.info/modules-intro#a-module-code-is-evaluated-only-the-first-time-when-imported covers our case, and has a simple example demonstrating it. It further states, “If the same module is imported into multiple other modules, its code is executed only once, upon the first import. Then its exports are given to all further importers.”

For my variation, I chose to use the following code for api.js:

import axios from "axios";

const axiosAPI = axios.create({
  baseURL: "http://localhost:3500/Posts",
});

export default axiosAPI;

And in code that uses it:

import axiosAPI from "./api/api";
...
        const res = await axiosAPI.get("/");

I think it is a little easier to understand. Ref: Creating an Axios Instance, https://dev.to/ndrohith/creating-an-axios-instance-5b4n

...

In Blog tutorial app. (my coding variation), I explored scrolling component scrollbar to top on render (SinglePost as well PostsList).

Manipulating the DOM with a ref, https://react.dev/reference/react/useRef#manipulating-the-dom-with-a-ref

In SinglePost, we need parent of the component top-level div (class SinglePost) for scrolling content display to top. But the parent is in SharedLayout: a div (class main).

So in SinglePost useEffect() we first get the DOM node of its top-level div (class SinglePost) through a useRef (value of current property of useRef associated object). Then we access the parentElement of this SinglePost top-level div, which is the SharedLayout div (class main).

Finally we use ScrollTo() method of this SharedLayout div (class main) Element to scroll the blog post body content to top of that div.

The main code in SinglePost.js to scroll to top on render:

    const postBodyDiv = useRef();
    useEffect(() => {
      console.log(postBodyDiv);
      if (postBodyDiv.current !== undefined) {
        const postMainDiv = postBodyDiv.current.parentElement;
        console.log(postMainDiv);
        if (postMainDiv !== undefined) {
          postMainDiv.scrollTo({
            top: 0,
            behavior: "smooth",
          });
        }
      }
    }, []);
  
  ...
  
  return (
      
        <div ref={postBodyDiv} className="SinglePost">
        ...
  )

Addl. Info:


-----------------------------

Home page (which uses PostsList component) scroll issue on render is a little different. It should remember the scroll position when user clicked on a post and when user comes back (i.e. Home is rendered again), the scroll position should be set to the remembered scroll position. Blogger works that way.

In the tutorial app. (as it is now), the scroll bar position on return to Home from SinglePost is based on scroll bar position in the SinglePost page just before user clicks Posts or Back to Posts. [I expected the same issue for return from Edit page for long posts where scrollbar is moved down but that does not seem to be the case (as I don't see the visual effect of scrollbar coming back to top on return to Home page, as I can see when user returns from SinglePost with scrollbar moved down).]

However, I think this issue need not be handled now. It will be too time-consuming for a tutorial project. But I could scroll to top always. Did that and it works.

I had presumed that having the scroll to top code in SharedLayout component would not work as SharedLayout would not be getting rendered again as user navigates between Home, SinglePost, New etc. pages. But I wanted to be sure about this. So I wrote the scrollTo code in SharedLayout where this code was simpler as the div with class main is directly in SharedLayout.

const mainRef = useRef();
useEffect(() => {
  console.log(mainRef);
  if (mainRef.current !== undefined) {
    mainRef.current.scrollTo({
      top: 0,
      behavior: "smooth",
    });
  }
}, []);

console.log("Shared Layout being rendered ...");

return (
    <div className="App">
        <Header
        title={process.env.REACT_APP_HEADER_CAPTION || "React Blog Full Stack"}
        />
        <div ref={mainRef} className="main">    
	...

I also added a console log statement everytime SharedLayout renders. The test confirmed that my presumption was right. SharedLayout is not rendered again as I switched between Home and SinglePost. Canceling) from New does not rerender SharedLayout but CreatePost from new does render SharedLayout again. I don't want to spend more time on analysis of this behaviour as it is clear that having the scrollTo code in SharedLayout does not seem to be the solution.

Comments

Archive