Read the React documentation for Hooks. Pay special attention to the useState and useEffect hooks.
TBD
In the first session we created a single page app using vanilla js. In this class we will use Preact which is (for our purposes) identical to React - just much smalled and faster. We will also explore a Create React App alternative. Vite is simpler and faster. The setup instructions are in the Get Started section.
The builds will be much smaller:
React:
46.9 kB (-1 B) build/static/js/main.d2f0e90f.js
Preact:
dist/assets/index-BeomYdVh.js 15.28 kB │ gzip: 6.34 kB
Here are the differences between React and Preact.
cd into your class working folder (top level, not this repo) and run:
npm create vite@latest - set the project name to "all-the-news-vite", select Preact as the framework, JavaScript as the variant, and follow the instructions:
cd vite-project
npm install
npm run devExamine the application structure.
- there is no
ejecthere - the number of dependencies is drastically reduced
index.htmlis not in thepublicfolder- we see the use of
.jsxand lower-case naming for files which contain components.
Vite does not use Webpack to run. Instead it leverages native browser modules - <script type="module" src="/src/main.jsx"></script>.
Copy the css and img directories from today's download and add them to the public folder in the newly created app.
Add a link tag to index.html to load the custom fint:
<link
href="https://fonts.googleapis.com/css?family=Lobster&display=swap"
rel="stylesheet"
/>Begin by creating a simple functional component in a components folder:
src/components/Header.js:
const Header = (props) => {
return (
<header>
<h1>{props.siteTitle}</h1>
</header>
);
};
export default Header;Import and compose it in App.jsx:
import Header from "./components/Header";
function App() {
return (
<>
<Header siteTitle="All the News that Fits We Print" />
</>
);
}
export default App;Create src/components/Nav.js in the components folder:
const Nav = (props) => {
return (
<nav>
<ul>
<li>Nav component</li>
</ul>
</nav>
);
};
export default Nav;Import into App.jsx and compose it:
import Header from "./components/Header";
import Nav from "./components/Nav";
function App() {
return (
<>
<Header siteTitle="All the News that Fits We Print" />
<Nav />
</>
);
}
export default App;In App.jsx add our nav items:
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];And send them, via props, to the Nav component:
import Header from "./components/Header";
import Nav from "./components/Nav";
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];
function App() {
return (
<>
<Header siteTitle="All the News that Fits We Print" />
<Nav navItems={NAVITEMS} />
</>
);
}
export default App;Use the Preact developer tool to inspect the Nav component and ensure the navItems props exists and are available in Nav.js.
Now we can build out the nav items using props:
const Nav = (props) => {
return (
<nav>
<ul>
{props.navItems.map((navItem) => (
<li key={navItem}>
<a href={`#${navItem}`}>{navItem}</a>
</li>
))}
</ul>
</nav>
);
};
export default Nav;Note the use of a template string above to add a hash to the href.
Note that when calling .map((navItem) here we are not using curly { ... } but rounded braces ( ... ). All the code could exist on a single line and we are using the arrow function's implicit return.
Create src/components/Stories.jsx component:.
const Stories = (props) => {
return (
<div className="site-wrap">
<pre>
<code>{JSON.stringify(props.stories, null, 2)}</code>
</pre>
</div>
);
};
export default Stories;JSON.stringify(props.stories, null, 2) will take our stories data and dump it into the UI. We've added <pre> and <code> tags to make it more readable. This is a very common technique often used when you prefer to examine the data in the UI instead of the console.
Import and compose the Stories component in app.jsx:
import Header from "./components/Header";
import Nav from "./components/Nav";
import Stories from "./components/Stories";
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];
function App() {
return (
<>
<Header siteTitle="All the News that Fits We Print" />
<Nav navItems={navItems} />
<Stories />
</>
);
}
export default App;We want to store the stories data in app.jsx.
Let's begin by creating two pieces of state in app.jsx:
import Header from "./components/Header";
import Nav from "./components/Nav";
import Stories from "./components/Stories";
import { useState } from "preact/hooks";
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];
function App() {
// HERE
const [stories, setStories] = useState([]);
const [loading, setLoading] = useState(false);
return (
<>
<Header siteTitle="All the News that Fits We Print" />
<Nav navItems={navItems} />
<Stories />
</>
);
}
export default App;We initialize stories as an empty array and add a bit of state to track whether the data is loading.
Note: importing hooks in Preact:
import { useState } from "preact/hooks";
is different from React:
import { useState } from "react";
Also note that the render function is imported from preact in main.jsx:
import { render } from "preact";Add variables for the api key and set a default section.
Import and use the useEffect hook to fetch the data and then pass it to the Stories component as a prop.
We also pass the stories state to the Stories component.
app.jsx:
import Header from "./components/Header";
import Nav from "./components/Nav";
import Stories from "./components/Stories";
import { useState, useEffect } from "preact/hooks";
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];
// HERE
const FETCH_URL = "https://api.nytimes.com/svc/topstories/v2/";
const NYT_API = "KgGi6DjX1FRV8AlFewvDqQ8IYFGzAcHM";
const section = "arts";
export function App() {
const [stories, setStories] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
.then((response) => response.json())
.then((data) => setStorageAndState(data.results));
}, []);
function setStorageAndState(data) {
localStorage.setItem(section, JSON.stringify(data));
setStories(data.results);
}
return (
<>
<Header siteTitle="All the News that Fits We Print" />
<Nav navItems={NAVITEMS} />
<Stories stories={stories} />
</>
);
}Ensure that arts is in localstorage and that it is in state using the Preact dev tool.
In computer science, a function or expression is said to have a "side effect" if it modifies some state outside its scope or has an observable interaction with its calling functions or the outside world besides returning a value. An "effect" is anything outside your application and includes things like cookies and fetching API data.
The useEffect Hook lets you perform side effects in React components. By using this Hook, you tell React that your component needs to do something after it renders. By default, it runs both after the first render and after every update but we'll be customizing it to run only when the section (arts, music etc.) changes. For now, we are only using one section - arts.
The full function signature looks like this:
useEffect(callbackFunction, []);Note the empty array that is the second argument in useEffect. An empty array causes the effect to run only once after the component renders and again when the component unmounts or just before it is removed. [] tells React/Preact that your effect doesn’t depend on any values from props or state, so it never needs to re-run.
Here is a possibly useful article on localStorage in React (hint: its not that different that in is in Vanilla JS).
Add an if/else statement similar to what we used in a previous exercise.
useEffect(() => {
if (!localStorage.getItem(section)) {
console.log("fetching from NYT");
fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
.then((response) => response.json())
.then((data) => setStories(data.results));
} else {
console.log("section is in storage, not fetching");
setStories(JSON.parse(localStorage.getItem(section)));
}
}, [section]);Notice, we added "section" to the dependency array.
Change the section variable:
const section = "books";
Note that the useEffect hook ran and the books data is in localStorage.
Rather than rendering everything in the stories component we'll pass that duty off to a component called Story.jsx (singluar).
Create src/components/Story.jsx:
const Story = (props) => {
return (
<div className="entry">
<p>Story component</p>
</div>
);
};
export default Story;Note: the stories are being passed to the Stories component: <Stories stories={stories} />
We will render multiple Story components from Stories.js with a key set to the story's index.
Stories.js:
import Story from "./Story";
const Stories = ({ stories }) => {
return (
<div class="site-wrap">
{stories.map((story, index) => (
<Story key={index} story={story} />
))}
</div>
);
};
export default Stories;Now, in Story.js, begin building out the content.
First the images:
const Story = (props) => {
return (
<div className="entry">
<img
src={
props.story.multimedia
? props.story.multimedia[1].url
: "/img/no-image.png"
}
alt="images"
/>
</div>
);
};
export default Story;And then the content:
const Story = (props) => {
return (
<div className="entry">
<img
src={
props.story.multimedia
? props.story.multimedia[1].url
: "/img/no-image.png"
}
alt="images"
/>
<div>
<h3>
<a href={props.story.short_url}>{props.story.title}</a>
</h3>
<p>{props.story.abstract}</p>
</div>
</div>
);
};
export default Story;Adjust the CSS grid:
.entry {
display: grid;
/* HERE */
grid-template-columns: 3fr 5fr;
grid-column-gap: 1rem;
margin-bottom: 1rem;
grid-area: "entry";
}Allow data to be fetched before we render the rest of the application.
Let's switch the setLoading piece of state to true while the fetch operation is under way and set it to false once the operation has completed.
Set setLoading to false by default in the initial declaration, then set it to true before the fetch operation, and finally set it back to false afterwards.
We'll also add an if statement that shows "Loading" while the data is loading.
import Header from "./components/Header";
import Nav from "./components/Nav";
import Stories from "./components/Stories";
import { useState, useEffect } from "preact/hooks";
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];
const FETCH_URL = "https://api.nytimes.com/svc/topstories/v2/";
const NYT_API = "PGQuh0auTqHC6HEx4gADBhT2yLCdXYbN";
const section = "books";
export function App() {
const [stories, setStories] = useState([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
.then((response) => response.json())
.then((data) => setStorageAndState(data));
}, []);
useEffect(() => {
if (!localStorage.getItem(section)) {
console.log("fetching from NYT");
// HERE
setLoading(true);
fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
.then((response) => response.json())
.then((data) => setStories(data.results));
// HERE
setLoading(false);
} else {
console.log("section is in storage, not fetching");
setStories(JSON.parse(localStorage.getItem(section)));
}
}, [section]);
function setStorageAndState(data) {
localStorage.setItem(section, JSON.stringify(data.results));
setStories(data.results);
}
// HERE
if (loading) {
return <h2>Loading...</h2>;
}
return (
<>
<Header siteTitle="All the News That Fits We Print" />
<Nav navItems={NAVITEMS} />
<Stories stories={stories} />
</>
);
}If you leave the loading state true after the fetch you should see the early return.
Set it to false at the end of the useEffect function:
setLoading(false);
(We can also see the loading block by slowing loading in the Network tab of the developer tools.)
Next we will make the section dynamic. Remove the section variable (const section = "books") and add it to the state.
// const section = "books";
...
const [section, setSection] = useState("arts");While we are at it we will use useEffect to store the section in localStorage when the stories change.
useEffect(() => {
console.log("setting localstorage");
localStorage.setItem(section, JSON.stringify(stories));
}, [stories]);This allows us to remove the initializing useState functionality in the first useEffect function.
// useEffect(() => {
// fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
// .then((response) => response.json())
// .then((data) => setStorageAndState(data));
// }, []);Here is the full App component with a .catch added at the end of the promise chain to log any errors that might occur:
import Header from "./components/Header";
import Nav from "./components/Nav";
import Stories from "./components/Stories";
import { useState, useEffect } from "preact/hooks";
const NAVITEMS = ["arts", "books", "fashion", "food", "movies", "travel"];
const FETCH_URL = "https://api.nytimes.com/svc/topstories/v2/";
const NYT_API = "PGQuh0auTqHC6HEx4gADBhT2yLCdXYbN";
export function App() {
const [stories, setStories] = useState([]);
const [loading, setLoading] = useState(false);
const [section, setSection] = useState("arts");
useEffect(() => {
if (!localStorage.getItem(section)) {
console.log("fetching from NYT");
setLoading(true);
fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
.then((response) => response.json())
.then((data) => setStories(data.results))
.catch((error) => console.error(error));
setLoading(false);
} else {
console.log("section is in storage, not fetching");
setStories(JSON.parse(localStorage.getItem(section)));
}
}, [section]);
useEffect(() => {
console.log("setting localstorage");
localStorage.setItem(section, JSON.stringify(stories));
}, [stories]);
if (loading) {
return <h2>Loading...</h2>;
}
return (
<>
<Header siteTitle="All the News That Fits We Print" />
<Nav navItems={NAVITEMS} />
<Stories stories={stories} />
</>
);
}Currently our app only renders one section. We need to code the navbar tabs to communicate with App.jsx in order to call fetch for additional sections.
Since clicking on the nav is what changes the section we'll pass setSection into the Nav in App.jsx:
return (
<>
<Header siteTitle="All the News that Fits We Print" />
{/* HERE */}
<Nav navItems={NAVITEMS} setSection={setSection} />
<Stories stories={stories} />
</>
);We'll use a new component in Nav to display each of the nav elements.
Create /src/components/NavItem.js:
const NavItem = (props) => {
return (
<li>
<a href={`#${props.navItem}`}>{props.navItem}</a>
</li>
);
};
export default NavItem;Import it and compose it in Nav.js:
import NavItem from "./NavItem";
const Nav = (props) => {
return (
<nav>
<ul>
{props.navItems.map((navItem, index) => (
<NavItem key={index} navItem={navItem} />
))}
</ul>
</nav>
);
};
export default Nav;Now we need to pass the setSection function from Nav.js to NavItem :
import NavItem from "./NavItem";
const Nav = (props) => {
return (
<nav>
<ul>
{props.navItems.map((navItem, index) => (
<NavItem
key={index}
navItem={navItem}
// HERE
setSection={props.setSection}
/>
))}
</ul>
</nav>
);
};
export default Nav;Back in NavItem we will create a local function sendSection and run it on an onClick event:
const NavItem = (props) => {
const sendSection = (section) => {
props.setSection(section);
};
return (
<li>
<a href={`#${props.navItem}`} onClick={() => sendSection(props.navItem)}>
{props.navItem}
</a>
</li>
);
};
export default NavItem;The click event now communicates with the setSection function in App.jsx and our useState hook runs again when the section changes:
// This code is already in app.jsx
useEffect(() => {
if (!localStorage.getItem(section)) {
console.log("fetching from NYT");
setLoading(true);
fetch(`${FETCH_URL}${section}.json?api-key=${NYT_API}`)
.then((response) => response.json())
.then((data) => setStories(data.results))
.catch((error) => console.error(error));
setLoading(false);
} else {
console.log("section is in storage, not fetching");
setStories(JSON.parse(localStorage.getItem(section)));
}
}, [section]);Note the [section] in the useFetch dependencies array.
The array allows you to determine when the effect will run. The empty array caused the effect to run once after the component rendered. When we add a piece of state or a prop to the array the effect will run whenever that state or prop changes.
Test it in the browser.
Add a highlight to the current nav item to indicate the section we are viewing.
We can use the section state to set the activeLink property.
Pass the section info to the Nav component.
In app.jsx:
<Nav navItems={NAVITEMS} setSection={setSection} section={section} />And then forward the property from Nav to the NavItem component:
import NavItem from "./NavItem";
const Nav = (props) => {
return (
<nav>
<ul>
{props.navItems.map((navItem, index) => (
<NavItem
key={index}
navItem={navItem}
setSection={props.setSection}
section={props.section}
/>
))}
</ul>
</nav>
);
};
export default Nav;Use the section in a ternary expression to set the class name:
const NavItem = (props) => {
const sendSection = (section) => {
props.setSection(section);
};
return (
<li>
<a
href={`#${props.navItem}`}
className={props.navItem === props.section ? "active" : ""}
onClick={() => sendSection(props.navItem)}
>
{props.navItem}
</a>
</li>
);
};
export default NavItem;The supporting CSS for this is in the public folder:
nav ul {
list-style: none;
display: flex;
justify-content: space-around;
align-items: center;
}
nav a {
text-decoration: none;
display: inline-block;
color: white;
text-transform: capitalize;
font-weight: 700;
padding: 0.75rem 1.5rem;
}
nav a.active {
box-shadow: inset 0 0 0 2px white;
border-radius: 6px;
}
nav a:not(.active):hover {
box-shadow: inset 0 0 0 2px white;
border-radius: 6px;
background-color: #00aeef;
}What would happen if our user copied the url from the browser and sent it to another person? Answer: the default "arts" section would be displayed. Not good. Let's fix that.
First, get the URL and convert the URL string into a URL object using the new URL() constructor.
export function App() {
const [stories, setStories] = useState([]);
const [loading, setLoading] = useState(false);
const [section, setSection] = useState("arts");
const url = new URL(window.location.href);
const hash = url.hash.slice(1);Do this within a useEffect hook in App.jsx:
useEffect(() => {
const url = new URL(window.location.href);
const hash = url.hash.slice(1);
if (hash !== "undefined") {
console.log("hash::", hash);
setSection(hash);
} else {
setSection("arts");
}
}, []);Examine the Local Storage in the browser's dev tools. Try reloading the browser. Your assignment os to fix the issue.