How to do an async fetch before initial render - javascript

I'm currently making use of the Wordpress API using Next.js on the front end. I want to fetch my navigation/menu data and have it prerendered. I've tried but only an empty <nav> </nav> element is rendered when I check the source-code. Is there a simple solution to this?
Here is my Nav component:
import { Config } from "../config";
import Link from "next/link";
import useFetch from "../hooks/useFetch";
export default function MainNav() {
const links = useFetch(`${Config.apiUrl}/wp-json/menus/v1/menus/main-nav`);
return (
<nav>
{!!links &&
links.map((link) => (
<Link href="/">
<a>{link.title}</a>
</Link>
))}
</nav>
);
}
And my custom useFetch.js hook:
import { useEffect, useState } from "react";
export default function useFetch(url) {
const [links, setLinks] = useState();
// Must use useEffect in non-page component
useEffect(async () => {
let res = await fetch(url);
res = await res.json();
setLinks(res.items);
}, []);
return links;
}

Firs of all useEffect is not async at all, even if you define async inside the useEffect callback function, the proper way to do this, is to set up separate state - useState(false) for spinner or include that into exisiting one, which will control the spinner, since you are fetching the data via REST, basically the full example should look like this:
useFetch.js hook:
import { useEffect, useState } from "react";
export default function useFetch(url) {
const [{ links, isLoading }, setLinks] = useState({ links: [], isLoading: true });
// Must use useEffect in non-page component
useEffect(() => {
(async funtion() {
const res = await fetch(url);
const { items } = await res.json();
setLinks({ links: items, isLoading: false });
})()
}, []);
return [isLoading, links];
}
Nav.js component:
import { Config } from "../config";
import Link from "next/link";
import useFetch from "../hooks/useFetch";
export default function MainNav() {
const [links, isLoading] = useFetch(`${Config.apiUrl}/wp-json/menus/v1/menus/main-nav`);
if(isLoading) {
return <Spinner/>
}
return (
<nav>
{!!links && !isLoading &&
links.map((link) => (
<Link href="/">
<a>{link.title}</a>
</Link>
))}
</nav>
);
}

So I figured it out, I fetched the links data on the page that the component was nested onto and then fed the data down using component composition. The problem is that I have to nest them all on the page directly. If someone has a more elegant solution, please let me know :)
The page index.js:
import PostIndex from "../components/PostIndex";
import Layout from "../components/Layout";
import Header from "../components/Header";
import MainNav from "../components/MainNav";
import { Config } from "../config";
export default function Home(props) {
return (
<Layout>
<Header>
<MainNav links={props.links} />
</Header>
<h2>Home Page</h2>
<PostIndex limit={3} />
</Layout>
);
}
export async function getServerSideProps() {
const [data1, data2] = await Promise.all([
fetch(`${Config.apiUrl}/wp-json/wp/v2/posts?per_page=3`),
fetch(`${Config.apiUrl}/wp-json/menus/v1/menus/main-nav`),
]);
const posts = await data1.json();
const links = await data2.json();
return {
props: {
posts,
links,
},
};
}
The Layout.js component:
export default function Layout({ children }) {
return <div>{children}</div>;
}
The Header.js component:
import Link from "next/link";
export default function Header({ children }) {
return (
<div>
<Link href="/">
<a>
<h1>Wordpress Blog</h1>
</a>
</Link>
{children}
</div>
);
}
And the MainNev.js component:
import { Config } from "../config";
import Link from "next/link";
export default function MainNav({ links }) {
return (
<nav>
{!!links &&
links.items.map((item) => (
<Link href="/">
<a>{item.title}</a>
</Link>
))}
</nav>
);
}

Related

How to pass context to _document.js in Next.js?

I want to change the thumb color of the scroll bar depending on the page I am on. Currently, I am using this script that I had made:
// /scripts/scrollbar.js
export default function updateScrollbar(thumbColor) {
const html = document.getElementsByTagName("html")[0];
html.style.setProperty("--thumb", thumbColor);
}
And if I am on some page, I have the following code to update the scroll bar thumb color
// /pages/Comp.js
import { useEffect } from "react";
import updateScrollbar from "../../scripts/scrollbar";
export default function Comp() {
useEffect(() => {
updateScrollbar("var(--purple)");
}, []);
return (
<p>hi</p>
);
}
This works fine. I visit a page and the scrollbar color updates.
I don't want to use to this scrollbar.js script though. So I tried the following:
// /Layouts/Layout.js
import { createContext, useState } from "react";
export const AppContext = createContext();
export default function Layout({ children }) {
const [thumbColor, setThumbColor] = useState("var(--white)");
const context = { thumbColor, setThumbColor };
return (
<AppContext.Provider value={context}>
{ children }
</AppContext.Provider>
)
}
I have created a context and passed thumbColor and setThumbColor as Provider's value.
// /pages/_app.js
import Layout from '../Layouts/Layout';
import '../styles/globals.css';
function MyApp({ Component, pageProps }) {
return (
<Layout>
<Component {...pageProps} />
</Layout>
)
}
export default MyApp;
The layout wraps the _app.js.
// /pages/Comp.js
import { useContext, useEffect } from "react";
import { AppContext } from "../../Layouts/Layout";
export default function Comp() {
const { setThumbColor } = useContext(AppContext);
useEffect(() => {
setThumbColor("var(--purple)");
}, []);
return (
<p>hi</p>
);
}
I get the set function from AppContext and want to update the thumbColor state from here.
import Document, { Html, Head, Main, NextScript } from "next/document";
import { AppContext } from "../Layouts/Layout";
class MyDocument extends Document {
static contextType = AppContext;
static async getInitialProps(ctx) {
const initialProps = await Document.getInitialProps(ctx);
return { ...initialProps }
}
render() {
let temp = this.context; // this is undefined
return (
<Html style={{ "--thumb": temp.thumbColor }} lang="en">
<Head>
</Head>
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
export default MyDocument;
Since context is undefined, I am not able to do anything. What am I doing wrong ?
Thanks for helping !

How to pass props via Link in React (this.props.location is undefined)

i am creating a recipe research project in react. On the home page I press 'search recipe' and it finds them for me, then 'view recipe' and it should show me some data that I have to decide. When in the component I go to do the console.log (this.props) it returns me all the object without the value of the state and therefore I cannot access the data. could you please help me? I leave you the code to understand better.
import logo from "./logo.svg";
import "./App.css";
import React, { useState } from "react";
import MealList from "./MealList";
function App() {
const [mealData, setMealData] = useState(null);
/*const [calories, setCalories] = useState(2000)*/
const [food, setFood] = useState("");
function handleChange(e) {
setFood(e.target.value);
}
function getMealData() {
fetch(
`https://api.spoonacular.com/recipes/complexSearch?apiKey=1d66c25bc4bb4ac288efecc0f2c4c4b8&diet=vegetarian`
) /* &addRecipeInformation=true */
.then((response) => response.json())
.then((data) => {
setMealData(data);
})
.catch(() => {
console.log("error");
});
}
return (
<div className="App">
<section className="controls">
{/*<input type="number" placeholder='Calories (e.g. 2000)' onChange={handleChange}/>*/}
<input type="string" placeholder="food" onChange={handleChange} />
</section>
<button onClick={getMealData}> CERCA PASTI VEGETARIANI</button>
{mealData && <MealList mealData={mealData}/>}
</div>
);
}
export default App;
import React from "react";
import Meal from "./Meal";
export default function MealList({ mealData }) {
return (
<main>
<section className="meals">
{mealData.results.map((meal) => {
return <Meal key={meal.id} meal={meal} />;
})}
</section>
</main>
);
}
import React, {useState, useEffect} from 'react'
import {Link} from 'react-router-dom'
export default function Meal({meal}) {
const [imageUrl, setImageUrl] = useState("");
useEffect(()=>{
fetch(`https://api.spoonacular.com/recipes/${meal.id}/information?apiKey=1d66c25bc4bb4ac288efecc0f2c4c4b8`)
.then((response)=>response.json())
.then((data)=>{
setImageUrl(data.image)
})
.catch(()=>{
console.log("errorn in meal js fetch")
})
}, [meal.id])
const location = {
pathname: '/somewhere',
state: { fromDashboard: true }
}
return (
<article>
<h1>{meal.title}</h1>
<img src={imageUrl } alt="recipe"></img>
<div>
<button className='recipeButtons'>
<Link to={{
pathname: `/recipe/${meal.id}`,
state: {meal: meal.id}}}>
Guarda Ricetta
</Link>
</button>
</div>
</article>
)
}
import React from "react";
class Recipe extends React.Component{
render() {
console.log(this.props)
return(
<div>class Recipe extends React.Component</div>
)
}
}
export default Recipe;
this is the result of console.log(this.props) (this.props.location is undefined):
this props
You haven't shown how you render <Recipe />, so I can't tell at a glance where the problem is.
However, you don't need to pass location as a prop. React-Router includes a hook, useLocation, which can be invoked from any function component. You can change Recipe to a function component and use:
import { useLocation } from 'react-router-dom'
/* ... */
function Recipe(props) {
const location = useLocation()
/* ... */
}
ETA:
Checking the type definitions for <Link/> and To, it appears the API reference on reactrouter.com is wrong. To is, in fact, string | Partial<Path>, where Path is:
interface Path {
/**
* A URL pathname, beginning with a /.
*
* #see https://github.com/remix-run/history/tree/main/docs/api-reference.md#location.pathname
*/
pathname: Pathname;
/**
* A URL search string, beginning with a ?.
*
* #see https://github.com/remix-run/history/tree/main/docs/api-reference.md#location.search
*/
search: Search;
/**
* A URL fragment identifier, beginning with a #.
*
* #see https://github.com/remix-run/history/tree/main/docs/api-reference.md#location.hash
*/
hash: Hash;
}
This is why the state is never being set. To set the state in the link, you need to include it as a React prop, like so:
<Link to={`/recipe/${meal.id}`} state={{ meal: meal.id }}>Guarda Ricetta</Link>
you can use functional component with react router hooks to access to the location instead of class component
import { useLocation } from "react-router-dom";
export default function Recipe () {
const location = useLocation();
return (
<div> Recipe </div
)
}

Global Invalid Hook Call - React

I recently started working with React, and I'm trying to understand why my context.js is giving me so much trouble. Admittedly I'm not great with JavaScript to start, so I'd truly appreciate any insight.
Thank you, code and the error that it generates:
import React, { useState, useContext } from 'react';
const AppContext = React.createContext(undefined, undefined);
const AppProvider = ({ children }) => {
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const openSidebar = () => {
setIsSidebarOpen(true);
};
const closeSidebar = () => {
setIsSidebarOpen(false);
};
const toggle = () => {
if (isSidebarOpen) {
closeSidebar();
} else {
openSidebar();
}
};
return (
<AppContext.Provider
value={{
isSidebarOpen,
openSidebar,
closeSidebar,
toggle
}}
>
{children}
</AppContext.Provider>
);
};
export const useGlobalContext = () => {
return useContext(AppContext);
};
export { AppContext, AppProvider };
Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
Thank you again for taking the time to look!
EDIT: Sidebar App Added for context (double entendre!)
import React from 'react';
import logo from './logo.svg'
import {links} from './data'
import {FaTimes} from 'react-icons/fa'
import { useGlobalContext } from "./context";
const Sidebar = () => {
const { toggle, isSidebarOpen } = useGlobalContext();
return (
<aside className={`${isSidebarOpen ? 'sidebar show-sidebar' : 'sidebar'}`}>
<div className='sidebar-header'>
<img src={logo} className='logo' alt='NavTask Management'/>
<button className='close-btn' onClick={toggle}>
<FaTimes />
</button>
</div>
<ul className='links'>
{links.map((link) => {
const { id, url, text, icon } = link;
return (
<li key={id}>
<a href={url}>
{icon}
{text}
</a>
</li>
);
})}
</ul>
</aside>
);
};
export default Sidebar;

React hook component renders before API call

I need to create a React app which let's you list pokemons and types.
I fetch data from the PokeAPI. Is it a good practice to fetch it from the App component and then pass it to the child components, or is it better to fetch them from the child?
I am fetching it in the main app, I can see the fetch works because I console.log the data, but my component doesn't get it, and because of that I get a props.map is not a function in .
Here is my App.js:
import React, { useState } from "react";
import logo from "./logo.svg";
import "./App.css";
import axios from "axios";
import { BrowserRouter as Router, Switch, Route, Link } from "react-router-dom";
import PokemonList from "./components/PokemonList";
const App = (props) => {
const [pokemons, setPokemons] = useState([]);
const [types, setTypes] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const getPokemons = () => {
const axios = require("axios").default;
axios.get("https://pokeapi.co/api/v2/pokemon").then(function (response) {
console.log("Fetched pokemons");
console.log(response.data.results);
setIsLoading(false);
setPokemons(response.data.results);
});
};
const getTypes = () => {
setIsLoading(true);
const axios = require("axios").default;
axios.get("https://pokeapi.co/api/v2/type").then(function (response) {
console.log("Fetched types");
console.log(response.data.results);
setIsLoading(false);
setTypes(response.data.results);
});
};
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/pokemons" onClick={getPokemons}>
Pokemons
</Link>
</li>
<li>
<Link to="/types">Types</Link>
</li>
</ul>
</nav>
{/* A <Switch> looks through its children <Route>s and
renders the first one that matches the current URL. */}
<Switch>
<Route path="/pokemons">
<Pokemons pokemons={pokemons} />
</Route>
<Route path="/types">
<Types />
</Route>
</Switch>
</div>
</Router>
);
};
function Pokemons(pokemons) {
return <PokemonList props={pokemons} />;
}
function Types(typeList) {
return <h2>TYPES:</h2>;
}
export default App;
Here is my PokemonList.js:
import React from "react";
import { Card } from "semantic-ui-react";
import PokeCard from "./PokeCard";
const Pokemonlist = (props) => {
let content = (
<Card.Group>
{props.map(function (object, i) {
return <PokeCard pokemon={object} key={i} />;
})}
</Card.Group>
);
return content;
};
export default Pokemonlist;
and last here is my PokeCard.js
import { Card, Image } from "semantic-ui-react";
import React from "react";
const PokeCard = (pokemon) => {
let content = (
<Card>
<Card.Content>
<Image floated="right" size="mini" src={pokemon.img} />
<Card.Header>{pokemon.name}</Card.Header>
<Card.Meta>{pokemon.base_experience}</Card.Meta>
<Card.Description>ID: {pokemon.id}</Card.Description>
</Card.Content>
</Card>
);
return content;
};
export default PokeCard;
So the basic idea is:
On the main page you click Pokemons button, which calls the fetch then renders the PokemonList component which basically just renders multiple PokeCard components from the data I fetched.
1, What am I missing here?
2, In my situation when nothing changes do I need to use useEffect?
3, When should I fetch the data, and where?
EDIT: I want to use hooks with zero classes
here is a summary of my answer
it is best to fetch some initial data in parent and then make further requests in child
component if necessary to save network usage
use the useEffect hook to fetch the results before rendering the elements
What you are missing is that you are not using props in pokemon and you should put the get call inside useEffect hook in App component because the child component is rendering before the props is passed to it and this is the reason you are getting undefined error

useContext give error Cannot read property '...' of undefined

I need help with this issue, my app component as in the image below. I want to store track object inselectedTrack in the state using useState when I click on the view details button. Then use it to display track details in instead of making another fetch from API to get tack details, but when I use useContext inside give me this error TypeError: Cannot read property 'selectedTrack' of undefined.
React Components
import React from 'react';
import Header from './Header';
import Search from '../tracks/Search';
import Tracks from '../tracks/Tracks';
import Footer from './Footer';
import TrackContextProvider from '../../contexts/TrackContext';
const Main = () => {
return (
<div>
<TrackContextProvider>
<Header />
<Search />
<Tracks />
<Footer />
</TrackContextProvider>
</div>
);
};
export default Main;
TrackContext.js
import React, { createContext, useState, useEffect } from 'react';
export const TrackContext = createContext();
const TrackContextProvider = props => {
const [tracks, setTracks] = useState([]);
const [selectedTrack, setSelectedTrack] = useState([{}]);
const API_KEY = process.env.REACT_APP_MUSICXMATCH_KEY;
useEffect(() => {
fetch(
`https://cors-anywhere.herokuapp.com/https://api.musixmatch.com/ws/1.1/chart.tracks.get?chart_name=top&page=1&page_size=10&country=fr&f_has_lyrics=1&apikey=${API_KEY}`
)
.then(response => response.json())
.then(data => setTracks(data.message.body.track_list))
.catch(err => console.log(err));
// to disable the warning rule of missing dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// state for heading
const [heading, setHeading] = useState(['Top 10 Tracks']);
return (
<TrackContext.Provider value={{ tracks, heading, selectedTrack, setSelectedTrack }}>
{props.children}
</TrackContext.Provider>
);
};
export default TrackContextProvider;
import React, { Fragment, useContext } from 'react';
import { Link } from 'react-router-dom';
import { TrackContext } from '../../contexts/TrackContext';
const TrackDetails = () => {
const { selectedTrack } = useContext(TrackContext);
console.log(selectedTrack);
return (
<Fragment>
<Link to="/">
<button>Go Back</button>
</Link>
<div>
{selectedTrack === undefined ? (
<p>loading ...</p>
) : (
<h3>
{selectedTrack.track.track_name} by {selectedTrack.track.artist_name}
</h3>
)}
<p>lyrics.............</p>
<div>Album Id: </div>)
</div>
</Fragment>
);
};
export default TrackDetails;
import React, { useState, useContext, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { TrackContext } from '../../contexts/TrackContext';
const Track = ({ trackInfo }) => {
const { selectedTrack, setSelectedTrack } = useContext(TrackContext);
const handleClick = e => {
setSelectedTrack(trackInfo);
};
console.log(selectedTrack);
return (
<li>
<div>{trackInfo.track.artist_name}</div>
<div>Track: {trackInfo.track.track_name}</div>
<div>Album:{trackInfo.track.album_name}</div>
<div>Rating:{trackInfo.track.track_rating}</div>
<Link to={{ pathname: `/trackdetails/${trackInfo.track.track_id}`, param1: selectedTrack }}>
<button onClick={handleClick}>> View Lyric</button>
</Link>
</li>
);
};
export default Track;
UPDATE: adding Tracks component
import React, { useContext, Fragment } from 'react';
import Track from './Track';
import { TrackContext } from '../../contexts/TrackContext';
const Tracks = () => {
const { heading, tracks } = useContext(TrackContext);
const tracksList = tracks.map(trackInfo => {
return <Track trackInfo={trackInfo} key={trackInfo.track.track_id} />;
});
return (
<Fragment>
<p>{heading}</p>
{tracks.length ? <ul>{tracksList}</ul> : <p>loading...</p>}
</Fragment>
);
};
export default Tracks;
I think the issue here is that since the selectedTrack is loaded asynchronously, when it is accessed from the context, it is undefined (you can get around the TrackContext being undefined by passing in a default value in the createContext call). Since the selectedTrack variable is populated anychronously, you should store it in a Ref with useRef hook, and return that ref as part of the context value. That way you would get the latest value of selectedTrack from any consumer of that context.
const selectedTracks = useRef([]);
useEffect(() => {
fetch(
`https://cors-anywhere.herokuapp.com/https://api.musixmatch.com/ws/1.1/chart.tracks.get?chart_name=top&page=1&page_size=10&country=fr&f_has_lyrics=1&apikey=${API_KEY}`
)
.then(response => response.json())
.then(data => {
selectedTrack.current = data.message.body.track_list;
})
.catch(err => console.log(err));
// to disable the warning rule of missing dependency
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

Categories