I am new to React. I don't understand why it's showing an error. I need to repeat the array object but when It reaches the last array it does not restart.https://codesandbox.io/s/generate-quote-xpu1q
index.js:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
app.js:
import React, { useState, useEffect } from "react";
import "./styles.css";
// import { qutoes } from "./Fetch";
export default function App() {
// const data = qutoes;
const [data, setData] = useState("loading");
const [index, setIndex] = useState(0);
const qutoesBtn = () => {
if (index === data.length - 1) {
setIndex(0);
} else {
setIndex(index + 1);
}
};
useEffect(() => {
fetch("https://type.fit/api/quotes")
.then(res => res.json())
.then(res2 => {
console.log(res2.slice(0, 10));
const lists = res2.slice(0, 10);
setData(lists);
});
}, []);
return (
<div className="App">
<h1>Generate Quote</h1>
<h4>
{data === "loading" ? (
"loading...!!"
) : (
<div>
My qutoes is ---- <br />
<span> {data[index].text}</span> <br /> Author is --
{data[index].author}
</div>
)}
</h4>
<button onClick={qutoesBtn}>Generate Quote</button>
</div>
);
}
You should change your condition to update once the index reaches data.length - 1
if (index === (data.length - 1)) {
setIndex(0);
}
Remember that given an array [1, 2, 3] the length is 3, but the maximum index is 2 because arrays indexes start at 0, so when index is equal to the data.length React is going to try to access that position giving you the error that you're experiencing.
It is because you are not checking if the array has reached the last item in the button click, So if you don't check it in the button click then it will increment the index again before it even gets to check , So change your code to this:
const qutoesBtn = () => {
if (index === data.length - 1) {
setIndex(0);
} else {
setIndex(index + 1);
}
};
Here -1 refers to the last item in the array
I think you need a condition where if the array reaches the end
Then start it back at zero
You wanna Check if the next item exceeds the length and you want to check it inside the qutoesBtn
const qutoesBtn = () => {
setIndex(index + 1);
if (index + 1 >= data.length) {
setIndex(0);
}
console.log(index);
};
CodeSandbox here
Related
I am doing a small project with react lifecycles and I am attempting to change all box colors randomly every time the counter reaches a number divisible by 5 however, the boxes only change when the number is divisible by 10.
App.js:
import React from 'react';
import './App.css';
import Counter from "./components/Counter";
import Box from "./components/Box";
function App() {
const randomColor = [
"#0d0d8c",
"#a41313",
"#064826",
"#996e00"
];
const [number, setNumber] = React.useState(0);
const [boxes, setBoxes] = React.useState([]);
const [color, setColor] = React.useState('red');
const [change, setChange] = React.useState(false);
let box = boxes.map((obj, idx) =>
<Box key={idx} color={color} />
);
let getColor = () => {
if (boxes.length % 5 === 0) {
let rand = Math.floor(Math.random() * randomColor.length);
setColor(randomColor[rand]);
return randomColor[rand];
}
return color;
};
React.useEffect(() => {
if (boxes.length % 5 === 0) {
let rand = Math.floor(Math.random() * randomColor.length);
setColor(randomColor[rand]);
return randomColor[rand];
}
return color;
}, [color])
React.useEffect(() => {
if (number % 2 === 0) {
let newBoxList = [...boxes];
newBoxList.push({color: getColor()});
setBoxes(newBoxList);
}
}, [number,change]);
let reset = () => {
setNumber(0);
setBoxes([]);
setChange(true);
};
return (
<div className="App">
<button onClick={() => setNumber(number + 1)}>Increase</button>
<button onClick={reset}>reset</button>
<Counter count={number}/>
<div className="boxes">{box}</div>
</div>
);
}
export default App;
deployed site: https://priceless-spence-9ae99a.netlify.app/
I think the culprit is your useEffect() hook listening to number. Whenever number is even you are copying the existing contents of boxes and then adding new boxes to the existing boxes; you are not actually updating all boxes in the array. It seems your desire is to have all of those boxes, existing and new, be updated to a new color so you'll need to iterate through the array and update each.
I think it comes down to the fact that you are assigning the return value of getColor to your new boxes rather than the value of local state color.
I'm trying to write what I thought would be a simple component: it takes an array of strings, and every three seconds the text in the div of the component is changed to the next item in the array, looping back to the beginning.
Although the console shows that the change message function is run every three seconds, the message never changes. I presume this is because the useState update never happens. Why is this, and how do I get it to work?
// components/textCycle.js
import { useState, useEffect} from 'react';
function TextCycle ( { array } ) {
const [ msg, setMsg ] = useState(0);
useEffect(() => {
function changeMsg() {
setMsg((msg > array.length) ? 0 : msg + 1);
}
setInterval( changeMsg, 3000);
}, []);
return (
<div>
{ array[msg] }
</div>
);
};
export default TextCycle;
// components/textCycle.js
import { useState, useEffect} from 'react';
function TextCycle ( { array } ) {
const [msg, setMsg] = useState(0);
function changeMsg() {
setMsg((msg > array.length - 2) ? 0 : msg + 1);
}
useEffect(() => {
setTimeout(changeMsg, 1000);
}, [msg]);
return (
<div>
{array[msg]}
</div>
);
};
export default TextCycle;
In a React & Next.js app I'm trying to implement a back button. To do that I've added currentPath and prevPath to the session storage in the _app.js file.
// pages/_app.js
function MyApp({ Component, pageProps }) {
const router = useRouter();
useEffect(() => {
const storage = globalThis?.sessionStorage;
if (!storage) return;
storage.setItem('prevPath', storage.getItem('currentPath'));
storage.setItem('currentPath', globalThis.location.pathname);
}, [router.asPath]);
return <Component {...pageProps} />
}
export default MyApp
Then I am trying to get this data in a Navigation.js component.
// Navigation.js
const router = useRouter();
const [prevPath, setPrevPath] = useState('/');
useEffect(() => {
const getPrevPath = globalThis?.sessionStorage.getItem('prevPath');
setPrevPath(getPrevPath);
}, [router.asPath]);
return (
// …
<Link href={prevPath || '/'}>
<a>Back</a>
</Link>
//…
)
While the session storage works correctly, the value returned is one from the previous page (that is previous page's prevPath) instead of the current one. Technically, asking for a currentPath instead of a prevPath would be the solution to what I'm trying to do but I'd like to (learn to) do it the right way.
Additional info:
I've tried to get data with async/await but it didn't make any difference.
useEffect(async () => {
const getPrevPath = await globalThis?.sessionStorage.getItem('prevPath');
setPrevPath(getPrevPath);
}, [router.asPath]);
Also, earlier in a day (the implementation was different) I've tried as an experiment adding a delay of 1/1000th of a second and it did make it work correctly. Given that, I'm not confident waiting a fixed number of seconds (or a fixed fraction of a second) would be a good solution (could someone confirm?).
Would appreciate the help.
Problem
I'm assuming you want to add and remove history (similar to a real browser history) instead of just constantly replacing the history with whatever route was previous. Instead of constantly replacing the pathname upon a route change, you'll want to conditionally add/remove it from some sort of history.
Solution
Here's a hook that utilizes an Array (basically a flat array of asPath strings -- you may want to limit the size of the Array to prevent performance issues):
import * as React from "react";
import { useRouter } from "next/router";
const usePreviousRoute = () => {
const { asPath } = useRouter();
// initialize history with current URL path
const [history, setHistory] = React.useState([asPath]);
const lastHistoryIndex = history.length - 2;
// get second to last route in history array
const previousRoute = history[lastHistoryIndex > 0 ? lastHistoryIndex : 0];
const removeHistory = () => {
// get current history
setHistory((prevHistory) =>
// check if the history has more than 1 item
prevHistory.length > 1
// if it does, remove the last history item
? prevHistory.filter((_, index) => index !== prevHistory.length - 1)
// else don't remove any history
: prevHistory
);
};
React.useEffect(() => {
// get current history
setHistory((prevHistory) =>
// check if the last history item is the current path
prevHistory[prevHistory.length - 1] !== asPath
// if not, add current path to history
? [...prevHistory, asPath]
// else don't add any history
: prevHistory
);
}, [asPath]);
return { previousRoute, removeHistory };
};
export default usePreviousRoute;
With capped history:
React.useEffect(() => {
// get current history
setHistory((prevHistory) =>
// check if last history item is current path
prevHistory[prevHistory.length - 1] !== asPath
// if not...
? [
// check if history has more than 10 items
// spread result into shallow copied array
...(prevHistory.length > 9
// if it does have more than 10 items, remove first item
? prevHistory.filter((_, index) => index !== 0)
// else don't remove history
: prevHistory),
asPath
]
// else don't remove history
: prevHistory
);
}, [asPath]);
Demo
Source Code:
Browser Demo URL: https://knfoj.sse.codesandbox.io/
Demo Code
Navigation.js
/* eslint-disable jsx-a11y/anchor-is-valid */
import * as React from "react";
import Link from "next/link";
import { useHistoryContext } from "../../hooks/useRouteHistory";
import GoBackLink from "../GoBackLink";
import styles from "./Navigation.module.css";
const Navigation = () => {
const { history } = useHistoryContext();
return (
<>
<nav className={styles.navbar}>
{[
{ title: "Home", url: "/" },
{ title: "About", url: "/about" },
{ title: "Example", url: "/example" },
{ title: "NoLayout", url: "/nolayout" }
].map(({ title, url }) => (
<Link key={title} href={url} passHref>
<a className={styles.link}>{title}</a>
</Link>
))}
</nav>
<GoBackLink />
<div className={styles.history}>
<h4 style={{ marginBottom: 0 }}>History</h4>
<pre className={styles.code}>
<code>{JSON.stringify(history, null, 2)}</code>
</pre>
</div>
</>
);
};
export default Navigation;
useRouteHistory.js
import * as React from "react";
import { useRouter } from "next/router";
export const HistoryContext = React.createContext();
export const useHistoryContext = () => React.useContext(HistoryContext);
export const usePreviousRoute = () => {
const { asPath } = useRouter();
const [history, setHistory] = React.useState([asPath]);
const lastHistoryIndex = history.length - 2;
const previousRoute = history[lastHistoryIndex > 0 ? lastHistoryIndex : 0];
const removeHistory = () => {
setHistory((prevHistory) =>
prevHistory.length > 1
? prevHistory.filter((_, index) => index !== prevHistory.length - 1)
: prevHistory
);
};
React.useEffect(() => {
setHistory((prevHistory) =>
prevHistory[prevHistory.length - 1] !== asPath
? [...prevHistory, asPath]
: prevHistory
);
}, [asPath]);
return { history, previousRoute, removeHistory };
};
export const HistoryProvider = ({ children }) => {
const historyProps = usePreviousRoute();
return (
<HistoryContext.Provider
value={{
...historyProps
}}
>
{children}
</HistoryContext.Provider>
);
};
export default HistoryProvider;
_app.js
import * as React from "react";
import HistoryContext from "../hooks/useRouteHistory";
const App = ({ Component, pageProps }) => (
<HistoryContext>
<Component {...pageProps} />
</HistoryContext>
);
export default App;
index.js
import Layout from "../components/Layout";
const IndexPage = () => (
<Layout>
<h1>Index Page</h1>
<p>
...
</p>
</Layout>
);
export default IndexPage;
import { useState, useEffect } from 'react'
import './App.css';
function App() {
const [count, setCount] = useState(0)
useEffect(() => {
console.log('render')
}, [count])
First: show me on UI but send me error on conosle: Too many re-renders. React limits the number of renders to prevent an infinite loop.
const plusCount = () => {
setCount(count + 1) }
const minsCount = () => {
setCount(count - 1) }
Second : do not sho em on UI send me error on UI: Too many re-renders. React limits the number of renders to prevent an infinite loop.
const makeCount = {
add:setCount(count + 1),
discount: setCount(count - 1)
}
return (
<h1>Exercise</h1>
<p>Cunt: <b>{count}</b></p>
<button onClick={plusCount}>Add</button>
<button onClick={minsCount}>Discount</button>
</div>
)
}
export default App;
Guestion:
Why is this message show me error on both time, but on first let me show on UI
on the second do not show me on UI
You are executing the setCount function on render, which causes a rerender which results in an infinity loop:
const makeCount = {
add: setCount(count + 1),
discount:setCount(count - 1)
}
This object actually call the setCount function instead of creating an fucntion to be called.
You need to change it to:
const makeCount = {
add: () => setCount(count + 1),
discount: () => setCount(count - 1)
}
This will generate new functions called add and discount instead of calling setCount.
App.js
import React ,{useState} from 'react';
import { Child } from './Components/Child';
function App() {
let value = [1,2,4,6];
const number = (number,val)=>{
console.log(`${number}: value ${val}`)
}
return (
<div className="App">
{
value.map((item , i)=>{
return <Child count = {item} itemName={i} key={i} muFunc={number}/>
})
}
</div>
);
}
export default App;
Child.js
import React,{useState,useEffect} from 'react';
export function Child ({count,itemName,muFunc}) {
const [number, setnumber] = useState(count);
useEffect(() => {
muFunc(itemName,number);
}, [number]);
const makeCount = {
add: () => setnumber(number + 1),
discount: () => setnumber(number - 1)
}
// Send this number to parent ??
return(
<>
<h3>{itemName}</h3>
<button onClick ={makeCount.discount}>decrement</button>
<input value={number} onChange={(e)=>setnumber(e.target.value)} />
<button onClick ={makeCount.add}>Increment</button>
<br/>
</>
)
}
In my nextjs app I am trying to create a slider where I display a different component each time the next button is clicked. This is my code
import React, { useState } from 'react';
import ResiliationForm from './ResiliationForm';
import ResiliationUserData from './ResiliationUserData';
const slidePages = [
{"id": 0, "page": "resiliationForm"},
{"id": 1, "page": "resiliationUserDate"},
{"id": 2, "page": "resiliationReview"},
]
const ResiliationSlider = ({ ...props }) => {
props.currentPage = slidePages[0];
const onNextClicked = value => {
let currentPage = props.currentPage.id;
props.currentPage = slidePages[currentPage + 1];
}
return (
<div>
{props.currentPage.id === 0 && <ResiliationForm key={props.currentPage.id} data={props.data} onNextClicked={onNextClicked}/>}
{props.currentPage.id === 1 && <ResiliationUserData key={props.currentPage.id} data={props.data} onNextClicked={onNextClicked}/>}
</div>
)
}
export default ResiliationSlider;
The onNextClicked is being called. I am chaning the current page in the props, but only the first page is shown. It seems that the return function is called only once. Can I force the element to be updated when the props is updated?
useEffect is designed for that. After rendering component && the value(a.k.a dependency) has changed , then execute useEffect function. Otherwise, skip the whole useEffect function.
import React, {useEffect} from 'react'
const ResiliationSlider = ({ ...props }) => {
props.currentPage = slidePages[0];
const onNextClicked = value => {
let currentPage = props.currentPage.id;
props.currentPage = slidePages[currentPage + 1];
}
useEffect(() => {
onNextClicked(value)
}, [value])
return (
<div>
{props.currentPage.id === 0 && <ResiliationForm key={props.currentPage.id} data={props.data} onNextClicked={onNextClicked}/>}
{props.currentPage.id === 1 && <ResiliationUserData key={props.currentPage.id} data={props.data} onNextClicked={onNextClicked}/>}
</div>
)
}
export default ResiliationSlider;
You should use the useState hook to track the state inside the component, rather than modifying the props. Changing the state will trigger a re-render.
const ResiliationSlider = ({ ...props }) => {
const [currentPage, setCurrentPage] = useState(slidePages[0])
const onNextClicked = value => {
let currentPage = currentPage.id
setCurrentPage(slidePages[currentPage + 1])
}
return (
<div>
{currentPage.id === 0 && <ResiliationForm key={currentPage.id} data={props.data} onNextClicked={onNextClicked}/>}
{currentPage.id === 1 && <ResiliationUserData key={currentPage.id} data={props.data} onNextClicked={onNextClicked}/>}
</div>
)
}