Static generation and SWR in single page - javascript

I'm building a blog website in Next.js, the API for the blog is from some headless CMS.
In a page I want to do the following:
List some blogs.
Set of buttons available, based on each button click different set of blogs are loading (should replace the blogs in #1).
Since SEO is needed I'm pretty confused to use which approach should I choose.
What I thinking that I generate the initial list with
getStaticProps (Static Generation), and after loading I want to replace the blogs based on user action (button click).
But I'm confused, is it possible to use static generation and SWR in single page?
Here is my implementation.
pages/index.js
export async function getStaticProps() {
const resPosts = await fetch(`${process.env.API_BASE_URL}posts?per_page=4&&_embed`)
const posts = await resPosts.json()
return {
props: {
posts
},
revalidate:10
}
}
export default function Home({posts}) {
return (
<div>
//pass data to FeaturedBlogs component (Components/featuredBlogs.js)
<FeaturedBlogs categories={categories} posts={posts} />
</div>
)
}
Components/featuredBlogs.js
const FeaturedBlogs = ({posts }) => {
return (
<div className={`${blogStyles.feature_blogs_wrap}`}>
//need to load the below blogs based on button click
<button onClick={handleClick('health')}>Health</button>
<button onClick={handleClick('latest')}>Latest</button>
//listing blogs
{posts.map((item ) => (
<Link key={item.id} href="/blog/view" passHref={true}>
<section>
<Image alt="blog_img" src={item._embedded['wp:featuredmedia'][0].media_details.sizes.medium.source_url} width="200" height="200" />
<div className={`${blogStyles.feature_blogs_content}`}>
<div className={`${blogStyles.feature_blogs_label}`}>
<span>{item._embedded['wp:term'][0][0].name}</span>
</div>
<p>{item.title.rendered}</p>
<div className={`${blogStyles.feature_blogs_author}`}>
<Image alt="author" src={item._embedded.author[0].avatar_urls[48]} width="200" height="200" />
<span>{item._embedded.author[0].name}</span>
</div>
</div>
</section>
</Link>
))}
</div>
)
}
const handleClick = (id) => {
//console.log(id)
}
What I need is to load the blogs in handleClick event, but the problem is this will not work since it's generated from the server at build time.

In the FeaturedBlogs component, you can create a state variable to keep track when a new category is selected on the client-side.
const [category, setCategory] = useState()
You can then make useSWR conditionally fetch data based on the value of this category variable.
const { data, loading } = useSWR(category ? [category] : null, fetcher)
The fetcher function would have the logic to fetch the posts for a given category.
const fetcher = async (category) => {
const response = await fetch(/* Endpoint to get posts for given category */)
return await response.json()
}
With this in place, you can have the component render the posts retrieved in getStaticProps as a default, when category is not set. This would happen on the initial render of the page. However, when a button is clicked, and category gets set, that category's data will be fetched and rendered instead.
Here's the full code of a modified version of your original component.
// Components/featuredBlogs.js
const fetcher = async (category) => {
const response = await fetch(/* Endpoint to get posts for given category */)
return await response.json()
}
const FeaturedBlogs = ({ posts }) => {
// Add state variable to keep track of the selected category
const [category, setCategory] = useState()
// Fetch posts from category only if `category` is set
const { data, loading } = useSWR(category ? [category] : null, fetcher)
const handleClick = (cat) => () => {
setCategory(cat)
}
// If `category` is set render data with post for given category, otherwise render all posts from `getStaticProps`
const itemsToRender = category ? data : posts
return (
<div className={blogStyles.feature_blogs_wrap}>
<button onClick={handleClick('health')}>Health</button>
<button onClick={handleClick('latest')}>Latest</button>
{loading && <div>Loading...</div>}
{!!itemsToRender?.length && itemsToRender.map((item) => (
<!-- Render items here -->
))}
</div>
)
}

Related

Issue with 1st render of async local JSON data

I'm having an issue when it comes to rendering some data out in my React app. I have a page /users that renders a list of users and clicking on a specific user routes the client to a page with more info on that user /users/:id. That page has a component with user data inside of it and that data is being fetched from a local JSON file. The issue is that when I click on one of the users from the /users page, the page for that specific user breaks. Console is telling me userData is undefined.
I have attempted to render the specific user page once the data has been fetched but I don't think I'm doing it correctly. I have tried setting an isLoading state with useState as well as conditionally rendering the component based on the state of the data being fetched but I'm still not having much luck. Below is the User page and the UserInfo component.
User page
function User() {
const [userData, setUserData] = useState([]);
const { id } = useParams();
const fetchData = async () => {
const response = await fetch(`../data/userData/${id}.json`);
const data = await response.json();
setUserData(data);
};
useEffect(() => {
fetchData;
}, []);
return (
<div>
{userData ? (
<UserInfo userData={userData} />
) : (
<>
<h1>Loading...</h1>
</>
)}
</div>
);
}
UserInfo component
function UserInfo({ userData }) {
return (
<div className='userInfo__details'>
<div className='userInfo__name'>
<h1>{userData[0].name}</h1>
</div>
</div>
);
}
The page is being rendered before the userData is being retrieved by the fetch request and that's causing the page to break. Ideally I'd like to have a loading spinner or something while the data is retrieved but anytime I've been able to having else condition render to indicate the data is still being fetched, it just hangs there and never actually renders the page with the fetched data.
Any help is appreciated.
[] as your default state is will return as true which renders the UserInfo component before time.
You can do this instead
return (
<div>
{!!userData.length ? (
<UserInfo userData={userData} />
) : (
<>
<h1>Loading...</h1>
</>
)}
</div>
);

Axios and getRequest from external component

I'm new to React.js and I'm actually trying to create a page structured in the following way:
-A form to insert [version,date,image,content] and post a JSON via POST request (with Axios)
-A display part in which I GET the same data and display on screen by clicking on a button
actually I'm able to do this by introducing the Get and Post logic in the used component.
In order to use Components and have a clear code, i would to have a separate component to call inside the various components to make a GET or POST request.
By using hooks I'm not able to do this. The actual code is:
This is UserGet.js
import axios from "axios";
import {useState} from "react";
const baseURL = "http://localhost:8088/";
const userURL ="changelog/version.txt";
function UserGet(props) {
const [get, setGet] = useState(null);
axios.get(baseURL+userURL).then((response) => {
setGet(response.data);
});
return [get, setGet] ;
}
export default UserGet;
while in the component I want to use as data displayer:
const DisplayInfo =(props) => {
const [items, setItems] = useState([]);
const onFinish = (() => {
setItems(UserGet());
})
const DisplayData = items.map(
(info)=>{
return(
<div className='changelogDisplay' key={info.version}>
<button className='version' >v {info.version} </button>
<div className='datelog' > {info.data} </div>
<div className='dataHeader' > {info.title} </div>
<div className='whatsnew' > {info.text} </div>
<div className='imageLog' alt='Changelog pic' >{info.img} </div>
</div>
)
});
return(
<div>
<Form onFinish={onFinish}>
<Form.Item>
<Button type="primary" htmlType="submit" name='Refresh'> Refresh list</Button>
<div className='displayData'>
{DisplayData}
</div>
</Form.Item>
</Form>
</div>
)
}
export default DisplayInfo;
Actually, if I use
const response = UserGet();
I'm able to get data and insert into items for further process. I want to get the data using Onfinish or Onclick properties but I'm not able due to Hooks that cannot stay inside a function. How can I get a solution?
I tried different ways, and I don't want to use Redux at the moment in order to improve my knowledge on this.
Hooks are not so simple to me
Currently you're setting the items state to the [get, setGet] value that returns from the UserGet hook.
Here you return the data back from the request.
async function UserGet(props) {
const response = await axios.get(baseURL + userURL);
return response.data;
}
Then in the onFinish
const onFinish = (() => {
const newItems = UserGet();
setItems(newItems);
// or
// setItems(UserGet());
});
I hope this helps you with your project!

Not able to see output, when reload the page. Output come just for a sec, then coming map error

Above code outputs basic Netflix movie name and images. Output comes just for a sec, then some error is coming on console related with maps, however it seems fine to be. what is the error here?
I used ReactJS here with useeffect hook, tried it for netflix clone
Row.js:
function Row({ fetchUrl, title }) {
const baseURL = "https://image.tmdb.org/t/p/original/"
const [movies, setMovies] = useState([])
useEffect(() => {
async function fetchData() {
const request = await axios.get(fetchUrl)
setMovies(request.data.results)
return request
}
fetchData()
}, [fetchUrl])
return (
<div className="row">
<h2> {title} </h2>
{movies.map((movie) => (
<img src={`${baseURL}${movie.poster_path}`} alt={movie.name} />
))}
</div>
)
}
requests.js:
const API_KEY = "ecfc81ae98ad1c0720b07e83400de828"
const requests = {
fetchTrending: `/trending/all/week?api_key=${API_KEY}&language=en-US`,
fetchNetflixOriginals: `discover/tv?api_key=${API_KEY}&with_networks=213`
}
axios.js:
const instance = axios.create({
baseURL : "https://api.themoviedb.org/3",
});
app.js:
function App() {
return (
<div ClassName="App">
<h1> Hey !! lets build Netflix</h1>
<Row title="Netflix Originals" fetchUrl={requests.netflixOriginals} />
<Row title="Trending" fetchUrl={requests.fetchTrending} />
</div>
)
}
Error:
try this
{movies && movies.map ((movie) => (
<img src={${baseURL}${movie.poster_path}} alt={movie.name}/>
))}
I believe you're querying genres endpoints, which return the movie list inside genres.
If I'm correct, you should replace
setMovies(request.data.results)
with
setMovies(request.data.genres)
Most importantly, console.log is your friend:
useEffect(() => {
axios.get(fetchUrl).then(r => console.log(r.data))
}, [fetchUrl])
Logging the exact data returned will help you figure out where the actual list is (and what you need to assign to movies).

how to control the state of many components

I have an api with which data comes about how many components there will be and what data will be stored in it, I generate these components through the map () method. Now I need that when I click on the image (which each component has), a class is added to this component, but at the moment I can only add an additional class to one element.
const [favorite, setFavorite] = useState();
// Function to add an additional class
async function addFavoriteChanel(index) {
if(favorite === index) {
setFavorite(0);
} else {
setFavorite(index)
}
}
// Getting data from api
async function getResponse() {
let response = await fetch('https:api.json')
let content = await response.json()
setChanelData(content.channels)
}
useEffect(() => {
getResponse();
}, [])
// visibleData it returns flickered data from api (Search engine, I did not add it here)
// ...img src={star}... Clicking on this image will add it to your favorites
<div className="container">
{Array.isArray(visibleData) ? visibleData.map((chanel, index) => {
return (
<div href={chanel.url} className="chanel__item" key={index}>
<img src={star} alt="star" onClick={() => addFavoriteChanel(index)} id={index} className={`star ${favorite === index ? 'active' : ''}`} />
<img src={chanel.image} alt="" className="chanel__img" />
<div className="chanel__title"><div className="chanel__item-number">{chanel.number}. </div>{chanel.name_ru}</div>
</div>
)
}) : null}
</div>
I thought I could use an object in state and add keys for each component there, but I don’t understand how to do it correctly
I'd be happy to hear any advice. =)
You should make the state an array, that way you can add multiple channels to favourites:
const [favorite, setFavorite] = useState([]);
// Function to add an additional class
function toggleFavoriteChanel(index) {
setFavorite(prevState => {
let returnArray = prevState;
if(prevState.includes(index)){
return returnArray.splice(prevState.indexOf(index), 1)
}else{
return [...returnArray, index]
}
}
}
and then you only need to change one more thing
<img src={star} alt="star" onClick={() => toggleFavoriteChanel(index)} id={index} className={`star ${favorite.includes(index) ? 'active' : ''}`} />
now you can add and remove from favourites by pressing the image
You need to split the content of .map into separate function component and declare state there

The data fetched from an API doesn't populate the table in NextJS

I try to populate a table with an API call in NextJS. I use getStaticProps function to get the data and pass them to the component. I tried map() function to access each data in the object but it doesn't work and doesn't populate the required data. when I run the app the console outputs the below error.
Warning: data for page "/" is 150 kB, this amount of data can reduce performance.
When I checked the page source it has the full API object and I don't understand why it does that. Below I have put the code of index.js and CountriesTable.js
index.js
import Head from "next/head";
import CountriesTable from "../components/CountriesTable/CountriesTable";
import Layout from "../components/Layouts/layout";
import SearchInput from "../components/SearchInput/SearchInput";
import styles from "../styles/Home.module.css";
export default function HomePage({ countries }) {
return (
<Layout>
<div className={styles.counts}>found {countries.length} countries</div>
<SearchInput placeholder="Search for country" />
<CountriesTable countries={countries} />
</Layout>
);
}
export const getStaticProps = async () => {
const res = await fetch("https://restcountries.com/v3.1/region/asia");
const countries = await res.json();
return {
props: {
countries,
},
};
};
CountriesTable.js
import styles from './CountriesTable.module.css';
export default function CountriesTable({countries}) {
return (
<div>
<div className={styles.heading}>
<button className={styles.heading_name}>
<div>Name</div>
</button>
<button className={styles.heading_population}>
<div>Population</div>
</button>
</div>
{countries.map((country) => {
<div className={styles.row}>
<div className={styles.name}>{country.name}</div>
<div className={styles.population}>{country.population}</div>
</div>
})}
</div>
);
};
How can I resolve the error in the console and populate the table with data? TIA!
NextJS is a server rendered platform. Any props you pass to pages/components will be serialized in the source itself. Therefore, it is very important to trim data to fields which are necessary. Otherwise you will see entire json in the HTML source. (Working codesandbox)
Reduce the amount of data returned from getStaticProps, getServerSideProps, or getInitialProps to only the essential data to render the page.
As only name & population are required we can create a new dataset with these two properties only. This should get rid of the warning as well.
export const getStaticProps = async () => {
const res = await fetch("https://restcountries.com/v3.1/region/asia");
const countries = await res.json();
const countriesWithNamePopulation = countries.map((country) => ({
name: country.name.common,
population: country.population
}));
return {
props: {
countries: countriesWithNamePopulation
}
};
};
countries.map needs a return statement
{countries.map((country) => {
return (
<div>
<div>{country.name}</div>
<div>{country.population}</div>
</div>
);
})}
I cannot reproduce your console warning but you are missing the return statement inside your map function.
This works for me:
{countries.map((country, index) => {
return(
<div key={index}>
<div>{country.name.common}</div>
<div>{country.population}</div>
</div>
)
})}
Furthermore you need cannot display country.name as it is an object. I used country.name.common which is valid as it is a string. Additionally you need to add a key property to your div inside the map function.

Categories

Resources