A JS library used to create single page apps (SPAs). With SPAs a server only ever needs to send a single HTML page to the browser and then React manages the whole website in the browser (i.e. website data, routing, user activity, etc.).
With routing (page to page navigation) the new page is not sent from the server, but rather React changes the content in the browser. This all makes the website work quickly.
- Using state
- React Router
- How and when to fetch data
- React Hooks (i.e.
useState,useEffect) - Create custom hooks
Navigate to project directory and run npx create-react-app my-app-name.
In the public folder you will find an index.html. This is the one HTML file that is served to the browser and where all of the React code is injected into this file within the div with the id of root.
The working or development code you create when building React app will go in the src folder. These will include the React components. The initial component created for us is the App.js file.
Within the src folder you will also see some CSS files, test files, reportWebVitals.js, and the index.js file. The index.js file kick starts the app. It is responsible for taking all of the React components and mounting them to the DOM. It does this by rendering the App.js component to the DOM at the div id of root in the HTML file. This makes the App.js file the root component.
// file: ./src/index.js
// You see below that root div of App.js is being rendered to the ReactDOM.
// React.StrictMode provides console warnings.
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);Components are self contained section of content (i.e. navbar, button, form, etc.).
Each component contains all of its own template and logic for that piece of content. The component will contain all of its template (The HTML to make up said component) as well as all of its own JS logic (i.e. A function that runs when a logout button is clicked).
Starting out we only have the one component, App.js (root component). It is a function named App which returns JSX. The JSX is converted into HTML templates via React dependencies when we save the file then renders the HTML to the DOM.
Note
At the bottom of React components you will export the component so that it can be used in other files (i.e. The App component is imported and used within the index.js file).
We can add dynamic data and variables to the component and add them to the DOM within the return statement inside curly braces.
React will convert the values to string before outputting it to the browser. The only things that React cannot output are booleans and objects. If you try to output an object you will get a runtime error stating it is an invalid React child.
You can also write dynamic values directly in the JSX via curly braces.
You can also store URLs in a variable and call them as dynamic values in the JSX.
The root component, App.js, sits at the top of the component tree. When making new components they get nested inside the root component on the tree. You can even nest more inside those components. This all makes up the component tree.
Within the src dir create a Navbar.js. Within this component use the React Snippets vscode extension to output a boiler pate stateless component or arrow function.
After component content created you import said component into the App.js.
You can have separate localized CSS files to a component as either a CSS module, CSS file, or styled components. For smaller apps you can simply have one global stylesheet (i.e. index.css).
Another method is inline styles. The difference between inline in JSX is that they are within curly braces instead of quotes.
If we were to add inline styles to the /create anchor element and add an object to it:
<a
href="/create"
style={{
color: "white",
backgroundColor: "#f1356d",
borderRadius: "0.5rem",
}}
>
New Blog
</a>You can add a function (i.e. handleClick()) within the component and bind it to an element by adding the event as an attribute to said element and making it equal to the dynamic value which will be the function reference (i.e. onClick={handleClick}).
For example:
// file: ./src/Home.js
const Home = () => {
// Function called on click event.
const handleClick = () => {
console.log("Heyoo!");
};
return (
<div className="home">
<h2>Homepage</h2>
<button onClick={handleClick}>Click Me</button>
</div>
);
};
export default Home;Tip
We only reference the function because if we invoked it then it will get called on page load and not on click. The click event will invoke it.
If we want to pass in an argument to the function then we have to do it differently because we do not want to invoke the function on load.
We have to wrap the event handler inside an anonymous function in order to accomplish this. The handler function will take a parameter (i.e. name) and we call the handler function with the argument for name:
// file: ./src/Home.js
// OTHER CODE...
const handleClickAgain = (name) => {
console.log(`Hello ${name}`);
};
// OTHER CODE...
<button
onClick={() => {
handleClickAgain("Mario");
}}
>
Click Me Instead
</button>;
// REST OF CODE...Tip
Since the above anonymous and handler call are a single expression then we can have it all on one line and remove the curly braces surrounding the handler function call (i.e. handleClickAgain()).
Since we are handling events we will have access to the event object. We can log it to the console when we add it as a param to the handler function.
For the second click event we do not have access to the event object right away with the handler function, but rather the anonymous function gains access to the event object right away and then it can be passed in as an argument to the handler function call. Once this is done the event can then be passed into the handler function as an argument.
For example:
// file: ./src/Home.js
// OTHER CODE...
const handleClickAgain = (name, event) => {
console.log(`Hello ${name}`, event.target);
};
// OTHER CODE...
<button onClick={(event) => handleClickAgain("Mario", event)}>
Click Me Instead
</button>;
// REST OF CODE...When we talk of the state of a component we mean the data being used by said component at that point in time.
In our Home.js component we will test changing a name variable on button click. If we update value of name in the handleClick function it will update the value of name, but it will not be rendered to the DOM.
It does not update in the DOM or template because the name variable created is not reactive (React does not watch it for changes). Nothing triggers React to re-render the template with the new value for name.
To trigger the change we use a hook called useState.
- We need to import the useState hook via destructuring from React into the component.
- Call the function
useStateinside the component and give it an initial value (i.e.useState("Mario")). - We need to store the useState function instead of just invoking it. We assign it to a const and use array destructuring to grab two values the
useStatehook provides us. The first value is the initial function (i.e.name) and the second is a function we can use to change that initial value (i.e.setName).
If we use name within the template it will grab whatever the value of that name is. If we want to change that value we would use the setName function to do it. The state value is reactive so if it changes it will change in the template too.
The useState hook can be used as many times as we want within a component to change values. The value within useState can be any data type.
For example:
// file: ./src/Home.js
// OTHER CODE...
const [name, setName] = useState("Mario");
const [age, setAge] = useState(25);
const handleClick = () => {
setName("Luigi");
setAge(34);
};
return (
// OTHER CODE...
<p>{name} is {age} years old.</p>
<button onClick={handleClick}>Change Name/Age</button>
// OTHER CODE...
)
// REST OF CODE...Note
The useState hook outputs an array of objects to the console. Each object represents the state that we have.
Provides us more functionality in the browser dev tools. In particular the components section provides us with a component tree.
If you click on a component you will see info such as props, hooks, rendered by, and source.
You will also see options for inspect DOM element, log data to console, and view the source file.
We will be setting state for the blogs because they may change in the future. We do this with an initial value of state set to an array of blog objects.
To add the blogs to the template we could simply hardcode 3 divs, but that would be redundant and if new blogs are created then it will not react to hardcoded containers.
A better strategy is to cycle through the blogs array using the map method.
- We take the
blogsproperty (state variable) and add it to the return statement within curly braces. - Use the
mapmethod on theblogsprop. Map fires a cb function for each item where we want to output some JSX template (For each iteration). - Assign the iteration to
blogwithin the cb function as an argument representing the current item we are iterating over. - For each iteration we want to output a
divwith a blog preview on the homepage which will display the title and author. - Each root element in the template we return (blog-preview div) needs to have a
keyattribute (Thekeyallows React to keep track of each item in the DOM as it outputs it or data is added/removed). Assigned toblog.idto be unique.
Using the blog mapping output as an example, we might have the same section on several different pages and we do not want repetitive code.
We resolve this by making that chunk of template its own reusable component. With it set as it's own component we can simply import it into the components we want to use it in (i.e. BlogList.js).
Sometimes we might want to use different data in the above component and we do this by passing in props. They allow us to pass data from a parent component into a child component (i.e. parent = Home.js, child = BlogList.js).
- Cut the
blogs.mapsection fromHome.jsand add it to the newBlogListcomponent. - Import and add
<BlogList />to the Home.js component. - Pass in a prop to the
BlogListcomponent insideHome.jscalledblogs. This will allow us to use the blogs data fromHome.jsin theBlogListcomponent. - Pass in blogs state variable (blogs data) into the above prop.
- We now have access to an argument inside the
BlogListcomponent defined inBlogList.js. The argument isprops. The property ofblogsfromHome.jswill now will be available on thepropsobject. - Now having access to the blogs prop we can create a variable within
BlogList.jsto access theblogs(const blogs = props.blogs;).
Looking at the props object in the console you will see it has the blogs property on it which is an array. Also you have the array of blogs (Get this due to the console.log(props, blogs)).
Note
Any blogs that we send through to a component are attached to the props object that allows us to access them.
You can pass in multiple props. For example back in Home.js we could pass in a title prop to the BlogList component (i.e. <BlogList blogs={blogs} title="All Blogs" />).
Now we can access that title prop in the BlogList.js component. We then can add that above the blogs within an h2 (i.e. <h2>{title}</h2>).
Tip
Where we call properties with the props object (const blogs = props.blogs;) we could instead destructure. This is done in the parenthesis instead of using (props) we would destructure from the props directly by telling it which properties you want ({blogs, title}). We can now remove the constants for blogs and title and the component will still work.
We can pass in different data to the BlogList component. Let's pass in title="Mario's Blogs" and set the blogs prop to filter for author of Mario.
Note
The filter method takes a callback function that returns a boolean true or false. If true then a new array is created with the truthy item(s).
If we wanted to be able to delete a blog we would add a button to the BlogList.js so that it shows up for each blog snippet.
We would add onClick to button and remember to wrap in an anonymous function so we can receive arguments. We need to pass in an ID to the function which is why we need it to be an argument. The ID will allow us to find the blog post and delete it.
- We create the
handleDeletefunction and pass it in as a prop to theBlogListcomponent (<BlogList blogs={blogs} title="Mario's Blogs" handleDelete={handleDelete} />). - Then back in
BlogList.jswe accept thehandleDeletefunction as a prop (Pass in function as a prop). We are invoking the function found in the parent home component. - Inside
handleDeletewe use thesetBlogssetter function to update the state to remove blog with corresponding ID. - Within the
handleDeletefunction we will create a constant callednewBlogsand assign it a filter on the blogs array. The filter will return a new array with only truthy values from the original array in it. Each iteration of the blogs array will take blog as an argument. True is if theiddoes not match theidinhandleDeleteargument and false if it does. Theidof the blog we want to remove is coming fromblog.idinBlogList.js. - Then the new value of
blogswill besetBlogs(newBlogs), which will also re-render UI.
Tip
We should not alter the BlogList's prop. Instead we make modifications where the data and state is held using setBlogs setter function.
For example:
// file: ./src/Home.js
const Home = () => {
const [blogs, setBlogs] = useState([
{ title: "My new website", body: "lorem ipsum...", author: "mario", id: 1 },
// OTHER CODE...
]);
const handleDelete = (id) => {
// Set new filtered array to newBlogs.
// Assign iteration to blog and filter out blog.id if it is equal to current id to delete it. If not equal to current id we add it to the newBlogs array.
const newBlogs = blogs.filter((blog) => blog.id !== id);
// Use the setter function from useState and assign it to the newBlogs array to re-render UI.
setBlogs(newBlogs);
};
return (
<div className="home">
{/* This is where we add handleDelete as a prop for BlogList.js to use. */}
<BlogList blogs={blogs} title="All Blogs!" handleDelete={handleDelete} />
</div>
);
};
// REST OF CODE...
// file: ./src/BlogList.js
// We grab handleDelete function as a prop from Home.js.
const BlogList = ({ blogs, title, handleDelete }) => {
return (
<div className="blog-list">
{/* OTHER CODE... */}
<button
{/* Wrap handleDelete() in anonymous function to only call handleDelete() function when clicked and not page load. */}
onClick={() => {
{/* Pass in blog.id as argument to handleDelete() function. We will be deleting based on the unique ID of the blog. */}
handleDelete(blog.id);
}}
>
Delete Blog
</button>
</div>
);
};
// REST OF CODE...This hook runs a function every render of the component. It renders on load and when state changes so useEffect runs code on every render mentioned above.
- Import the hook from React.
- Above return statement add
useEffect(). It does not need to be assigned to a variable and does not return anything. - Add anonymous function inside
useEffect()as an argument. This is the function that runs on each render.
Note
Usually inside the useEffect hook function we perform authentication or fetch data (side effects).
We can also access state inside useEffect (i.e. blogs).
Warning
Be careful not to update state inside the useEffect hook because you could end up in a continuous loop. There are ways around this.
Sometimes you do not want to run the useEffect hook every render so we would use a dependency array. This is passed into the hook as a second argument. An empty dependency array means that the function will only run once on the first render.
You can also add any state values that trigger a render to the dependency array.
For the following example we will create a new state for name, add a button that will change the name on click by using setName, and add the name state variable as the dependency to useEffect.
If we delete a blog or any other state change it will not work because it depends on name:
// file: ./src/Home.js
// OTHER CODE...
const Home = () => {
// OTHER CODE...
const [name, setName] = useState("Mario");
// OTHER CODE...
useEffect(() => {
console.log("Use Effect Ran");
console.log(name);
}, [name]);
return (
<div className="home">
{/* OTHER CODE... */}
<button onClick={() => setName("Luigi")}>Change Name</button>
<p>{name}</p>
{/* OTHER CODE... */}
</div>
);
};
export default Home;Allows us to utilize a fake REST API.
When using JSON Server each top level property is considered a resource (i.e. blogs). Endpoints are created to interact with the resource so we can do things like delete items, edit items, add items, get items, etc.
We use the JSON Server package to watch the file (db.json) and wrap it in some endpoints. We can either install the package locally or with npx to watch the db.json file.
After running: npx json-server --watch data/db.json --port 8000 to run JSON Server, watch db.json file, and run on port 8000 we will see an endpoint created at http://localhost:8000/blogs. If we are to perform a GET request now we would use the above endpoint. We will perform a fetch request within our component to get the data.
Tip
You can demo the GET request by pasting the endpoint in the browser to view the db.json data.
The endpoints we will be using are as follows:
| Endpoint | Method | Description |
|---|---|---|
| /blogs | GET | Fetch all blogs |
| /blogs/{id} | GET | Fetch a single blog |
| /blogs | POST | A a new blog |
| /blogs/{id} | DELETE | Delete a blog |
Now that we will be fetching data from the db.json file we can clear out the manual data within the blogs useState, set it to null for the initial state, and we will be utilizing the useEffect hook to fetch.
Once we successfully fetch the data we will update the state using the setBlogs setter function.
- Add
fetch()touseEffect(). - Add the endpoint inside a string within the fetch parenthesis.
- The fetch returns a
promiseso we can use a.then()which will run a function once the promise is resolved. - We get a response (
res) object (not the data) and to get the data we need to returnres.json()into res anonymous function. Theres.jsonpasses the data into the res object for us. This also returns apromisebecause it is asynchronous. - Add another
.then()which runs a function once theres.jsoncompletes. - Pass in the parameter of
datainto the second.then(). Logging the data to the console will show an array of two objects which are the blogs indb.json. - Update the
blogsstate usingsetBlogssetter function. We pass in data intosetBlogs()which is within the second.then()block. No infinite loop because of the empty dependency array. - Fix error of mapping over blogs at value of
nullinBlogList.js. This happens because it takes a bit of time to fetch the data. We wrap theBlogListcomponent in a dynamic block and addblogs &&before<BlogList />. This creates a logical AND logic and sinceblogsis falsy then we do not output the<BlogList />. The right side of&&will only output when the left side is true (We will do this a lot with templating).
Create an additional piece of state inside Home.js.
const [isLoading, setIsLoading] = useState(true);.
Now to do another conditional template like we did with {blogs && <BlogList />}. This time it will be {isLoading && <div>Loading...</div>}.
We only want it to show the loading message when the data is loading. When we receive the data we want to switch isLoading to false. This is done inside the useEffect hook after setBlogs(data).
{isLoading && <div>Loading...</div>}.
Tip
You can emulate the loading message either by wrapping the useEffect fetch in a setTimeout or using the network throttling in Chrome Dev Tools.
Common errors with fetch are connection errors or errors fetching data from server.
- Add a
catch()block to theuseEffecthook after the last.then(). Thecatch()block catches any network error (Cannot connect to server) and fires a function. - If request is denied or endpoint does not exist then we would check the
okproperty of theresobject in an if statement in the first.then(). If it is notokthen wethrowandErrorwith a message for the error. - Store the error in some kind of state by setting
errorandsetErrorforuseState. Initial value for state isnull. - Add
setError(err.message)to the catch block instead of console. - We can now do conditional rendering in the template to output the error message. Now only if we have a value for state variable
errorwill we output it. - Also add
isLoading(false)in the catch block since it is not actually loading when there is an error. - If we end up successfully fetching data in the future we want to remove the error message so we will add
setError(null)to the second.then()block.
When we throw an error then it is caught by the catch() block and the .message is logged to the console.
With the useEffect hook in Home.js we are updating state for blogs, loading message, and error. You can prevent having to write all of this code over again by creating a custom hook. This is done by externalizing the login into its own JS file to be imported into other components if needed.
- Create a new file in the
srcdir calleduseFetch.js. - Create a function to put all of the
useEffectanduseStatecode in fromHome.js. This is the hook. Custom hooks need to start with the word use. In this case,useFetch. - Copy
useEffectcode anduseStatecode fromHome.jsand paste insideuseFetch. Make sure to importuseEffectanduseStatefrom React as well as export defaultuseFetch. - Change
[blogs, setBlogs]to[data, setData]inuseFetch.jsbecause in another component it might not be blogs as the data we are fetching. Don't forget to change it inside theuseEffecthook as well. - Return some values as the bottom of the
useEffecthook. We will return an object (i.e. It can be an array or boolean). Inside the object we will add three props (data, isLoading, and error). We do this because we want to grab those three properties from the hook. - Next, we will pass the endpoint into the
useFetchfunction as an argument (url) versus hardcoding it as part of the fetch block. This is because it might not always be the same endpoint in another component we are using theuseFetchin. Make sure to add url to the fetch parenthesis as well. - Pass in the url as the dependency array for
useEffect(useEffect(() => {...}), [url]) so that whenever the URL changes it will re-run the function to get the data for the new endpoint. - Import the
useFetchfunction inside theHome.jscomponent. We do this by destructuring the three props from theuseFetchfunction (data, isLoading, and error). If we used an array for the returned props inuseFetch.jsthen the order would be required when importing intoHome.js(Therefore an object is more ideal).const {data, isLoading, error} = useFetch("http://localhost:8000/blogs");.
Tip
Now that the prop for blogs has been changed to data you can either change the blogs value in the conditional statement in Home.js to {blogs && <BlogList blogs={data} />} or change the value of data to blogs in the destructuring of the useFetch using a colon const {data: blogs} = useFetch("http://localhost:8000/blogs");.
- A regular multi page website sends a request to the server when you type in its URL.
- Server sends back an HTML page which we view.
- When a user clicks a link to another page on the site it sends a new request to the server.
- Server responds again by sending back the HTML page.
- Repeat each time a page is clicked on. Constant requests for pages from the server.
- React delegates all page changes and routing to the browser only.
- Starts the same way with initial request to the server.
- Server responds and renders HTML page to browser, BUT it also sends back the compiled/bundled React JS files which control the application.
- Now React takes full control of the app. Initially the page is empty and then React injects content dynamically using the created components.
- If a user then clicks on a page in the navigation React Router intercepts this, prevents new server request, and then looks at the request and inject the required content/component(s) on screen.
- Install React Router
pnpm install react-router-dom. - Import
BrowserRouter as Router,Route, andSwitchcomponents from React Router in theApp.jsfile. - Surround entire app with the Router component. This gives the entire app access to the router as well as all child components.
- We want the page content to go inside
<div className="content">...</div>when we go to different pages. So we will replace theHomecomponent inApp.jswith theSwitchcomponent. The switch component makes it so that only one route shows at a time. - We place each route inside the switch statement. Currently only have one route (Homepage). We add the
Routecomponent and thepathattribute to this component (i.e. For the homepage the path would be/). - Nest the component inside the route that we want to be injected when a user visits the route (i.e.
Homecomponent).
Note
When user visits / we want to render the Home component. Also, the Navbar component is always going to show because it is outside the Switch statement. It will show on every route.
If there were two routes and one was fetching data while we switch to a new one we would get an error in the console: 'Warning: Can't perform a React state update on an unmounted component'. This is because the data fetching did not complete and now the component that was performing the fetch is no longer displayed in the browser (The unmounted component is the one trying to fetch the data).
We want to stop the fetch once we navigate to a new route/component. This is done with both the cleanup function in useEffect hook and the abort controller.
- Go to the
useFetch.jsand add areturnfunction at the bottom of theuseEffecthook. - At the top of the
useEffecthook we will add the abort controller.const abortCont = new AbortController();. We will associate the abort controller with a fetch request so that we can use it to stop the fetch. - Add a second argument to fetch as signal and set signal to the abort controller.
fetch(url, { signal: abortCont.signal }). - Now we remove the console log from the cleanup function and add the
constant.abort()method.
When we abort a fetch it still throws an error which is caught in the catch block and we update the state. We are not updating the data anymore because the fetch has been stopped, but we are still updating the state. This means we are still trying to update the home component with that state.
- Update the catch block to recognize the abort and not update the state.
if (err.name === "AbortError") {console.log("Fetch Aborted");}. - Add the
setIsLoadingandsetErrorto an else statement in the same if statement.
Sometimes we use dynamic values as part of a route. The dynamic part of a route is the route parameter (Like a variable inside a route).
We can access route parameters inside our React app and components.
- Create a
BlogDetails.jsinside thesrcdir. - Add a new
RouteonApp.jsand the syntax for a route param is :param-name (i.e.<Route path="/blogs/:id" element={<BlogDetails />} />). Make sure you add theBlogDetailscomponent to theRoute.
Now if you navigate to /blogs/anything-here(id) you will see the blog details component no matter what you put in after /blogs/.
We want to be able to fetch the id inside the blog details component. We will use a hook (useParams) from react router to do this.
- Import
useParamsfromreact-router-dominside the blog details component. - Add a new constant to the top of the component and set it equal to
useParams(). - Destructure whatever params you want (We names our param
idin theRoutein theApp.jsfile). - Now you can add the dynamic
idto the template by addingidinside curly braces.
Now that we have access to the param id we can fetch data from that blog using said id.
- Now we will add links to the blog details component from the blogs listed on the Homepage.
- We have access to each blog inside the
mapfunction in theBlogList.js. Also, each blog in/data/db.jsonhas a correspondingidproperty. So we can wrap theh2andpelements inBlogList.jsinside a link using theidprop. - To the
Linkto value we will set it equal to curly braces and template literals because some of the value will be dynamic. Set it equal to/blogs/and variable containing blog frommapand.idfor each blog'sidprop.<Link to={``/blogs/${blog.id``}></Link>.
We will reuse the custom hook useFetch in the BlogDetails component to fetch data based on the id of the blog.
Take note that the useFetch component returns data, isLoading, and error.
We will use the hook (useFetch) in the blog details component and pass in the URL of the endpoint we want to fetch data from.
- Import
useFetchintoBlogDetails.js. - Add constant, destructure the three returned data from
useFetch(data, isLoading, etc.), and set it equal touseFetch('https://localhost:8000/blogs/' + id);. - Add a loading
divto the template using a conditional andisLoading. - Do the same as above for an error
div. - We want to have some template for the blog itself once we have blog details or a value for the blog (This starts as
nullinuseFetch). - Build up the blog template using a conditional like loading and error and add elements inside parenthesis (i.e. article, with blog title, author and body).
We will use forms to add to the blog data. This will require controlled inputs and different form fields. Controlled inputs are a way to setup form inputs in React so that we can track their value and store the value in some kind of state (State will be stored in Create.js). We can also make it so that if state changes we can update the value we see in the input field. Input field and state are kept in sync with each other.
- At the top of
Create.jswe will add state for the title with the initial value as an empty string:const [title, setTitle] = useState("");. - Associate the title state value with the value of the title input. Dynamically add title as a value for the text input for title:
<input type="text" required value={title} />. Whatever is in theuseStatefor title will show as the text input value, but it will not let us change the value. - We need to make it so that when we change the value for title it triggers the
setTitlesetter function and this re-renders so the value updates. - We add the
onChangeevent to the input for title and set it to an anonymous function that invokessetTitle. This changes the title when we change the input value:<input type="text" required value={title} onChange={() => setTitle()} />. - Since we get access to the event object inside the anonymous function above we can update the useState value whenever we type in the title input field. Using
e.target.value(Target is the title input and value is whatever we type into the target):<input type="text" required value={title} onChange={(event) => setTitle(event.target.value)} />. - We want to see the above changes so we will add a paragraph element at the bottom of the form and output the
titlevalue dynamically. - Repeat all steps for the body while changing from title to body accordingly.
- For the select element for author it is very similar for setting state except the initial state is equal to one of the author values.
- For
valueandonChangefor the author select element we do the same thing making sure we update from title to author. - Dynamically output author value at the bottom of the form as well.
When a button is pressed inside of a form it fires a submit event on the form itself. We can listen to that submit event and react to it.
Note
You can also attach click event to the button itself, but it is preferable to react to the submit event.
- Add the
onSubmitevent to the form element. - Create a
handleSubmitfunction at the top of the component and assign theonSubmitevent to it. - In the
handleSubmitfunction we pass in the event object and reference the function within the formonSubmitattribute. - Inside the
handleSubmitcode block we first prevent default action of the form submission (i.e. A page refresh):event.preventDefault(). - Next, we create a blog object. This is what generally is saved inside the
/data/db.json.
Note
When using JSON Server we do not need to provide an id prop to the blog object because when we make a post request JSON Server will create a unique id for us.
- Inside the handleSubmit function after prevent default, we add constant for blog and set it equal to an object.
- Inside the object for blog we add
title,blog, andauthor.
Now that we can grab the blog data on form submit we need to perform a POST request to JSON Server to add the data to /data/db.json.
You could modify the useFetch hook to handle the POST request, but we will instead add it directly to handleSubmit function in the Create.js since we are only going to make the request in one place in our app.
- In
Create.jshandleSubmitfunction we will add thefetchblock with the endpoint for all blogs. - There is a second argument we add to the
fetchblock where we tag on the data as well as define the request type (POST). - Within the second argument we add method of
POST,headers(For content type of JSON which will be sent with this request), andbodywhich is the data we will be sending. - In the
bodywe need to convert the data from an object to a JSON string using thestringifymethod. We pass in the data we want to convert which isblog. - The fetch is asynchronous so we can add a
.then()block to fire a function when this is complete. - Add in a loading state with an initial state of
false. It will be changed to true when we submit the form which is within thehandleSubmitfunction:setIsLoading(true);. - Then we want it to go back to false after submit so within the
.then()block. - We want to have one button in the template for when
isLoadingisfalse(i.e. Add Blog button) and one for whenisLoadingistrue(i.e. Loading... button which is disabled). This is done by adding curly braces around the button and when notisLoadingyou render the Add Blog button and when it isisLoadingyou render an Adding Blog button:{!isLoading && <button>Add Blog</button>}{isLoading && <button disabled>Adding Blog...</button>}.
For example:
// file: ./src/Create.js
// OTHER CODE...
const [title, setTitle] = useState("");
const handleSubmit = (event) => {
event.preventDefault();
const blog = { title, body, author };
// ? console.log(blog);
setIsLoading(true);
fetch("http://localhost:8000/blogs", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(blog),
}).then(() => {
console.log("New Blog Added");
setIsLoading(false);
});
};
// OTHER CODE...Once we complete the new blog submission we want to redirect the user back to the homepage. We can accomplish this by using another React Router hook called useNavigate.
The useNavigate hook allows us to go navigate the app imperatively. It does this in a fast way.
- Inside the
Create.jswe want to importuseNavigatefrom React Router DOM. - Invoke the
useNavigatehook:const navigate = useNavigate();. - We now have an object represented by
navigateconstant which we can pass in a route to redirect to. - Add the navigate method and homepage route to the
.then()block.
- Add a delete button at the bottom of the
BlogDetailscomponent template and attach a click event to it. - Assign a handle delete function to the
onClickevent of the button. - Inside the
handleDeletefunction we will make a fetch request and pass in theblogsendpoint as well as theidof the blog we want to delete (We have access toblog.idso that is what we will put at the end of the endpoint). - We now need the second argument inside fetch which is the object specifying request type (
DELETE). - Since fetch is
asyncwe can tack on a.then()block which will fire a function when request is completed. - After blog is deleted we want to redirect user to the homepage so we will use the
useNavigatehook and pass in the route for the homepage. This is all done within the.then()block function.
For example:
// file: ./src/BlogDetails.js
// OTHER CODE...
const handleDelete = () => {
fetch(`http://localhost:8000/blogs/${blog.id}`, {
method: "DELETE",
}).then(() => {
navigate("/");
});
};
// OTHER CODE...
<article>
<h2>{blog.title}</h2>
<p>Written by {blog.author}</p>
<div>{blog.body}</div>
<button onClick={handleDelete}>Delete</button>
</article>;When a user tries to navigate to a page/route that does not exist we will display this 404 page.
- Create a new component called
Notfound.js. - Add a stateless function to the above component with some content as well as a
Linkproperty from React Router to take user back to homepage. - We need to add a catch-all route for the
NotFoundcomponent toApp.js. This is done by adding a new route, withNotFoundcomponent and setting the element/path to an asterisks:<Route path="*" element={<NotFound />} />. The asterisks means to catch all other routes.