when addItems() function is called, I changed the loading state to true. but it always remains false. what I am missing here?
const Persons = props => {
const [data, setData] = useState([]);
useEffect(() => {
addItems();
}, []);
let [position, setPosition] = useState(
(props && props.match && props.match.params.placeholder) || 0
);
const [loading, setLoading] = useState(false);
const numOfItem = 4;
const addItems = () => {
setLoading(true);
const items = [...data];
for (let i = 0; i < numOfItem; i++) {
const newItem = getData[position];
// if no item is found than go out from that loop
if (!newItem) break;
position++;
items.push(newItem);
}
setData(items);
setPosition(position);
setLoading(false);
};
return (
<>
{data.map(p => (
<Person person={p} />
))}
<button onClick={addItems}>Quick Load More {numOfItem} items</button>
</>
);
};
in devtools, I see loading state is always remains false.
You might need to start the loading state as true and after set all the items change the loading state to false.
Related
In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.
When you first run the component, it should display "1", and on button click, append it by 3, which it does. Value inside localstorage also changes, but when i reload the page, localstorage changes again to 1... what am i missing?
import { useState, useEffect } from 'react'
function Test() {
const [testNum, setTestNum] = useState(1);
useEffect(() => {
const data = window.localStorage.getItem("MY_TEST_ITEM");
setTestNum(JSON.parse(data));
}, []);
useEffect(() => {
window.localStorage.setItem("MY_TEST_ITEM", JSON.stringify(testNum))
}, [testNum]);
return (
<div>
<h1>{testNum}</h1>
<button onClick={() => { setTestNum(testNum + 3) }}>Add</button>
</div>
)
}
export default Test
try this instead :
const [testNum, setTestNum] = useState(window.localStorage.getItem("MY_TEST_ITEM") === null ? 1 : window.localStorage.getItem("MY_TEST_ITEM"));
Every time you reload your page your state refreshes.
You have testNum set to 1 so every render it will be set to that.
const [testNum, setTestNum] = useState(1);
Then you have these useEffects running at the same time
useEffect(() => {
const data = window.localStorage.getItem("MY_TEST_ITEM");
setTestNum(JSON.parse(data));
}, []);
useEffect(() => {
window.localStorage.setItem("MY_TEST_ITEM", JSON.stringify(testNum))
}, [testNum]);
The second useEffect will render before the first one, thus setting the localStorage to 1.
UseEffect is actually not needed in this case.
Try this;
const storageValue = window.localStorage.getItem("MY_TEST_ITEM";
const [testNum, setTestNum] = useState(storageValue ?? 0 );
updateLocalStorageFunc(value){
setTestNum(value)
window.localStorage.setItem("MY_TEST_ITEM", JSON.stringify(testNum))
}
return (
<div>
<h1>{testNum}</h1>
<button onClick={() => { updateLocalStorageFunc(testNum + 3) }
</div>)
Basically you are saying give "testNum" the value 1 on every page refresh, that means your local storage will be always updated to the initial value on the refresh which is 1.
So the solution is very obvious, all what you need to do is change the initial state value which should be the localStorage value not the hard coded value.
const [loading, setLoading] = useState(true)
const [testNum, setTestNum] = useState(null)
const intialValue = 1
useEffect(() => {
const localStorageValue = window.localStorage.getItem("YOUR_ITEM_KEY") || intialValue
setTestNum(Number(localStorageValue))
setLoading(false)
}, [])
useEffect(() => {
testNum !== null && window.localStorage.setItem("YOUR_ITEM_KEY", testNum)
}, [testNum])
if(loading) return <div>Loading ...</div>
return(
<>
<div>testNum: {testNum}</div>
<button onClick={e => setTestNum(testNum + 3)}>increment testNum by 3</button>
</>
)
You can also use the useMemo hook to avoid any unnecessary re-renders
So I was trying to build a server side pagination that works like a static one and I'm almost there, But I've encountered some issues which I cannot seem to solve.
This is what my code looks like
const LiveIndex = (props) => {
const [currentPage, setCurrentPage] = useState(0);
const [isLoading, setLoading] = useState(false);
const startLoading = () => setLoading(true);
const stopLoading = () => setLoading(false);
useEffect(() => {
//After the component is mounted set router event handlers
Router.events.on("routeChangeStart", startLoading);
Router.events.on("routeChangeComplete", stopLoading);
return () => {
Router.events.off("routeChangeStart", startLoading);
Router.events.off("routeChangeComplete", stopLoading);
};
}, []);
const paginationHandler = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage + 1;
props.router.push({
pathname: currentPath,
query: currentQuery,
});
setCurrentPage(currentQuery.page);
};
const backToLastPage = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage - 1;
setCurrentPage(currentQuery.page); // THE code that breaks my code.
props.router.push({
pathname: currentPathh,
query: currentQueryy,
});
};
let content;
if (isLoading) {
content = (
<div>
<h2 class="loading-text">loading.</h2>
</div>
);
} else {
//Generating posts list
content = (
<div className="container">
<h2> Live Games - </h2>
<div className="columns is-multiline">
<p>{props.games.name}</p>
</div>
</div>
);
}
return (
<>
<div className={"container-md"}>
<div>{content}</div>
{props.games.length ? (
<a onClick={() => paginationHandler(currentPage)}> moore </a>
) : (
backToLastPage(currentPage)
)}
</div>
</>
);
};
export async function getServerSideProps({ query }) {
const page = query.page || 1; //if page empty we request the first page
const response = await fetch(
`exampleapi.com?sort=&page=${page}&per_page=10&token`
);
const data = await response.json();
return {
props: {
games: data,
},
};
}
export default withRouter(LiveIndex);
The issue is my backToLastPage does the job well but I'm unable to use setCurrentPage() in that function, Every time I use that I get the following error
Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop
How can I possibly update the value of my currentPage state in the backToLast function
Thank you
You're calling backToLastPage directly in JSX which will be re-rendered/re-called every time. And setCurrentPage (with useState) triggers re-rendering for state changes in backToLastPage.
You can imagine that every time the state changes, your component gets rendered and it will set states again that make infinite renderings for the component.
You can use useEffect to handle props.games changes. That will help you to trigger backToLastPage only once whenever props.games get changed.
React.useEffect(() => {
if(!props.games || !props.games.length) {
backToLastPage(currentPage)
}
},[props.games])
Full modification can be
const LiveIndex = (props) => {
const [currentPage, setCurrentPage] = useState(0);
const [isLoading, setLoading] = useState(false);
const startLoading = () => setLoading(true);
const stopLoading = () => setLoading(false);
useEffect(() => {
//After the component is mounted set router event handlers
Router.events.on("routeChangeStart", startLoading);
Router.events.on("routeChangeComplete", stopLoading);
return () => {
Router.events.off("routeChangeStart", startLoading);
Router.events.off("routeChangeComplete", stopLoading);
};
}, []);
//The main change is here
//It will be triggered whenever `props.games` gets updated
React.useEffect(() => {
if(!props.games || !props.games.length) {
backToLastPage(currentPage)
}
},[props.games])
const paginationHandler = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage + 1;
props.router.push({
pathname: currentPath,
query: currentQuery,
});
setCurrentPage(currentQuery.page);
};
const backToLastPage = (page) => {
const currentPath = props.router.pathname;
const currentQuery = props.router.query;
currentQuery.page = currentPage - 1;
setCurrentPage(currentQuery.page); // THE code that breaks my code.
props.router.push({
pathname: currentPathh,
query: currentQueryy,
});
};
let content;
if (isLoading) {
content = (
<div>
<h2 class="loading-text">loading.</h2>
</div>
);
} else {
//Generating posts list
content = (
<div className="container">
<h2> Live Games - </h2>
<div className="columns is-multiline">
<p>{props.games.name}</p>
</div>
</div>
);
}
return (
<>
<div className={"container-md"}>
<div>{content}</div>
{props.games.length && (
<a onClick={() => paginationHandler(currentPage)}> moore </a>
)}
</div>
</>
);
};
export async function getServerSideProps({ query }) {
const page = query.page || 1; //if page empty we request the first page
const response = await fetch(
`exampleapi.com?sort=&page=${page}&per_page=10&token`
);
const data = await response.json();
return {
props: {
games: data,
},
};
}
export default withRouter(LiveIndex);
This is my parent component:
const A = () => {
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const isFetching = useRef(false);
...
const fetchMoreData = async () => {
if(isFetching.current) return;
isFetching.current = true;
setIsLoading(true);
try {
const newData = await ...
setData([...data, ...newData]);
}
catch(err) {
...
}
setIsLoading(false);
isFetching.current = false;
}
...
return <B data={data} isLoading={isLoading} onEndReached={fetchMoreData} />
}
And I am trying to memoize my child component B, to avoid unnecessary re-renders. Currently, I am doing the following:
const B = memo(({ data, isLoading, onEndReached }) => {
...
return (
<FlatList
data={data}
isLoading={isLoading}
onEndReached={onEndReached}
/>
);
},
(prevProps, nextProps) => {
return JSON.stringify(prevProps.data) === JSON.stringify(nextProps.data) &&
prevProps.isLoading === nextProps.isLoading;
});
But, I think, that my memoization can cause problems... as I am not adding prevProps.onEndReached === nextProps.onEndReached
But... all works fine :/ Btw I suppose that there can be a little chance to see unexpected things happening because of not adding it. What do you think? Is it necessary to add methods in the areEqual method?
I'm building my site in React and I have created pagination and search. When I search for something on the site, it only works when after that I go to another page. I think this is due to the fact that Softwares and Pagination are in the same component.
Then I tried lifting-state-up, but I got an error: React Minified Error # 31.
Here's Pagination component:
const Paginator = ({
total, // Total records
startPage = 1,
totalPages = null,
onMovePage = null,
}) => {
...
return (
<>
<section id={styles.paginator}>
<Header/>
...
{range(1, totalPages+1).map(p => (
<PagItem key={p} handleClick={ () => {setCurrentPage(p); onMovePage && onMovePage({currentPage: p})} } title={p} name={p} />
))}
...
</section>
</>
);
};
Here's Softwares component:
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const fetchData = async ({ currentPage }) => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
useEffect(() => {
fetchData({ currentPage: 1 });
}, []);
return (
<>
{
valid &&
<section className={styles.softwares}>
<Header header={"new softwares"} />
{softwares.map(s => (
<Article key={s.id} pathname={s.id} title={s.title} image={s.image} pubdate={s.pub_date} icon={s.category.parent.img} categoryID={s.category.id} categoryName={s.category.name} dCount={s.counter} content={s.content} />
))}
<Paginator totalPages={totalPages} total={total} onMovePage={fetchData} />
</section>
}
</>
);
};
SearchForm in Header component:
const Header = ({ handleChange, handleClick }) => {
return (
...
<SearchForm handleChange={handleChange} handleClick={handleClick} />
...
);
};
const SearchForm = ({ style, handleChange, handleClick }) => {
return (
<div style={style}>
<form>
<input
type="text"
onChange={handleChange}
/>
<SearchButton onClick={handleClick} />
<small>ENTER</small>
</form>
</div>
);
};
const SearchButton = ({onClick }) => {
return (
<button type="button" onClick={onClick}>
<FontAwesomeIcon icon={faSearch} />
</button>
);
};
And part of Search in App component:
const App = () => {
...
// Search
const [search, setSearch] = useState('');
const [shouldFetch, setShouldFetch] = useState(false);
const handleChange = (e) => {
setSearch(e.target.value);
}
useEffect(() => {
if (shouldFetch) {
(async () => {
const response = await fetch(`http://127.0.0.1:8000/api/software/?search=${search}`);
const data = await response.json();
setShouldFetch(false);
})()
}
}, [shouldFetch]);
const handleClick = () => setShouldFetch(true);
return (
<div className="App">
<Header handleChange={handleChange} handleClick={handleClick} />
...
<Switch>
<Route path="/" exact render={props => <Softwares {...props} search={search} />} />
</Switch>
{/* Actually I'd like to use Paginator here, but it
throws the error: React Minified Error # 31 */}
...
</div>
);
}
So, how can this be done?
The problem is your useEffect dependencies (or lack thereof).
Here's the relevant section of the code:
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const fetchData = async ({ currentPage }) => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
useEffect(() => {
fetchData({ currentPage: 1 });
}, []);
The empty dependency array means that you are running the effect that calls fetchData one time when the component mounts. Clicks in the Pagination component will call the fetchData function directly. Changes to search do not cause fetchData to re-run. The data depends on the search so search should be a dependency.
The fetchData function is fine in this component. The state that I would recommend lifting up is to lift the currentPage up from Pagination into Softwares. The onMovePage callback can just update the currentPage state. That way you can call fetchData only through your effect and run the effect whenever either search or currentPage changes.
const Softwares = ({ search }) => {
const [softwares, setSoftwares] = useState([]);
const [total, setTotal] = useState(null);
const [totalPages, setTotalPages] = useState(null);
const [valid, setValid] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
useEffect(() => {
// defining the function inside of the useEffect
// lets eslint exhaustive dependency checks work their magic
const fetchData = async () => {
const SEARCH = search ? `?search=${search}` : '';
const CURRENT_PAGE = currentPage && SEARCH === '' ? `?page=${currentPage}` : '';
const response = await fetch(`http://127.0.0.1:8000/api/software/${CURRENT_PAGE}${SEARCH}`);
const data = await response.json();
setSoftwares(data.results);
setTotal(data.count);
setTotalPages(data.total_pages);
setValid(true);
}
// need to define and call in separate steps when using async functions
fetchData();
}, [currentPage, search]);
return (
...
<Paginator page={currentPage} totalPages={totalPages} total={total} onMovePage={setCurrentPage} />
...
);
};