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
useState
inside 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
useState
hook 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
blogs
property (state variable) and add it to the return statement within curly braces. - Use the
map
method on theblogs
prop. Map fires a cb function for each item where we want to output some JSX template (For each iteration). - Assign the iteration to
blog
within the cb function as an argument representing the current item we are iterating over. - For each iteration we want to output a
div
with 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
key
attribute (Thekey
allows React to keep track of each item in the DOM as it outputs it or data is added/removed). Assigned toblog.id
to 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.map
section fromHome.js
and add it to the newBlogList
component. - Import and add
<BlogList />
to the Home.js component. - Pass in a prop to the
BlogList
component insideHome.js
calledblogs
. This will allow us to use the blogs data fromHome.js
in theBlogList
component. - Pass in blogs state variable (blogs data) into the above prop.
- We now have access to an argument inside the
BlogList
component defined inBlogList.js
. The argument isprops
. The property ofblogs
fromHome.js
will now will be available on theprops
object. - Now having access to the blogs prop we can create a variable within
BlogList.js
to 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
handleDelete
function and pass it in as a prop to theBlogList
component (<BlogList blogs={blogs} title="Mario's Blogs" handleDelete={handleDelete} />
). - Then back in
BlogList.js
we accept thehandleDelete
function as a prop (Pass in function as a prop). We are invoking the function found in the parent home component. - Inside
handleDelete
we use thesetBlogs
setter function to update the state to remove blog with corresponding ID. - Within the
handleDelete
function we will create a constant callednewBlogs
and 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 theid
does not match theid
inhandleDelete
argument and false if it does. Theid
of the blog we want to remove is coming fromblog.id
inBlogList.js
. - Then the new value of
blogs
will 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
promise
so 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.json
passes the data into the res object for us. This also returns apromise
because it is asynchronous. - Add another
.then()
which runs a function once theres.json
completes. - Pass in the parameter of
data
into the second.then()
. Logging the data to the console will show an array of two objects which are the blogs indb.json
. - Update the
blogs
state usingsetBlogs
setter 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
null
inBlogList.js
. This happens because it takes a bit of time to fetch the data. We wrap theBlogList
component in a dynamic block and addblogs &&
before<BlogList />
. This creates a logical AND logic and sinceblogs
is 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 theuseEffect
hook 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
ok
property of theres
object in an if statement in the first.then()
. If it is notok
then wethrow
andError
with a message for the error. - Store the error in some kind of state by setting
error
andsetError
foruseState
. 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
error
will 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
src
dir calleduseFetch.js
. - Create a function to put all of the
useEffect
anduseState
code in fromHome.js
. This is the hook. Custom hooks need to start with the word use. In this case,useFetch
. - Copy
useEffect
code anduseState
code fromHome.js
and paste insideuseFetch
. Make sure to importuseEffect
anduseState
from React as well as export defaultuseFetch
. - Change
[blogs, setBlogs]
to[data, setData]
inuseFetch.js
because in another component it might not be blogs as the data we are fetching. Don't forget to change it inside theuseEffect
hook as well. - Return some values as the bottom of the
useEffect
hook. 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
useFetch
function 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 theuseFetch
in. 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
useFetch
function inside theHome.js
component. We do this by destructuring the three props from theuseFetch
function (data, isLoading, and error). If we used an array for the returned props inuseFetch.js
then 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
, andSwitch
components from React Router in theApp.js
file. - 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 theHome
component inApp.js
with theSwitch
component. 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
Route
component and thepath
attribute 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.
Home
component).
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.js
and add areturn
function at the bottom of theuseEffect
hook. - At the top of the
useEffect
hook 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
setIsLoading
andsetError
to 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.js
inside thesrc
dir. - Add a new
Route
onApp.js
and the syntax for a route param is :param-name (i.e.<Route path="/blogs/:id" element={<BlogDetails />} />
). Make sure you add theBlogDetails
component 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
useParams
fromreact-router-dom
inside 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
id
in theRoute
in theApp.js
file). - Now you can add the dynamic
id
to the template by addingid
inside 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
map
function in theBlogList.js
. Also, each blog in/data/db.json
has a correspondingid
property. So we can wrap theh2
andp
elements inBlogList.js
inside a link using theid
prop. - To the
Link
to 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 frommap
and.id
for each blog'sid
prop.<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
useFetch
intoBlogDetails.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
div
to 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
null
inuseFetch
). - 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.js
we 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 theuseState
for 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
setTitle
setter function and this re-renders so the value updates. - We add the
onChange
event 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
title
value 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
value
andonChange
for 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
onSubmit
event to the form element. - Create a
handleSubmit
function at the top of the component and assign theonSubmit
event to it. - In the
handleSubmit
function we pass in the event object and reference the function within the formonSubmit
attribute. - Inside the
handleSubmit
code 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.js
handleSubmit
function we will add thefetch
block with the endpoint for all blogs. - There is a second argument we add to the
fetch
block 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), andbody
which is the data we will be sending. - In the
body
we need to convert the data from an object to a JSON string using thestringify
method. 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 thehandleSubmit
function: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
isLoading
isfalse
(i.e. Add Blog button) and one for whenisLoading
istrue
(i.e. Loading... button which is disabled). This is done by adding curly braces around the button and when notisLoading
you render the Add Blog button and when it isisLoading
you 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.js
we want to importuseNavigate
from React Router DOM. - Invoke the
useNavigate
hook:const navigate = useNavigate();
. - We now have an object represented by
navigate
constant 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
BlogDetails
component template and attach a click event to it. - Assign a handle delete function to the
onClick
event of the button. - Inside the
handleDelete
function we will make a fetch request and pass in theblogs
endpoint as well as theid
of the blog we want to delete (We have access toblog.id
so 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
async
we 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
useNavigate
hook 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
Link
property from React Router to take user back to homepage. - We need to add a catch-all route for the
NotFound
component toApp.js
. This is done by adding a new route, withNotFound
component and setting the element/path to an asterisks:<Route path="*" element={<NotFound />} />
. The asterisks means to catch all other routes.