using React useEffect to fetch data and controll component - javascript

i am trying to query data using useEffect add those data to state and render them but nothing comes up unless a state in the app changes. this is what i have done so far Please help, Thanks in Advance.
useEffect
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
whole component
import { useDispatch, useSelector } from "react-redux";
import _ from "lodash";
import styles from "../../../styles/CreateLeads.module.css";
import {
getAgentCampaigns,
getCampaignLeads,
} from "../../../utils/requests/campaign";
import {
hideTopLoader,
showTopLoader,
} from "../../../store/actions/TopLoader/topLoaderActions";
import CampaignSection from "./CampaignSection";
import Empty from "../Empty/Empty";
import SectionHeader from "../SectionHeader/SectionHeader";
import SearchBar from "../SearchBar/SearchBar";
import { RootState } from "../../../store/store";
const CreateLeadsCardsWrapper: React.FC = () => {
const authToken = useSelector(
(store: any) => store.authenticationReducer.authToken
);
const [stateCampaigns, setStateCampaigns] = React.useState<any[]>([]);
const [showCampaigns, setShowCampaigns] = React.useState<boolean>(false);
const [campaigns, setCampaigns] = React.useState<any[]>(stateCampaigns);
const [filter, setFilter] = React.useState<string>("");
const dispatch = useDispatch();
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// Reset filter
setFilter("");
let campaignSearch = e.target.value.trim();
if (campaignSearch.length === 0) {
return;
}
let campaignSearchLower = campaignSearch.toLowerCase();
let campaignSearchUpper = campaignSearch.toUpperCase();
let campaignSearchSentence =
campaignSearch.charAt(0).toUpperCase() + campaignSearch.slice(1);
let results = stateCampaigns.filter(
({ leads }: { leads: any[] }, i) =>
leads &&
leads?.some(
(lead: any) =>
lead.firstName.includes(campaignSearch) ||
lead.firstName.includes(campaignSearchLower) ||
lead.firstName.includes(campaignSearchUpper) ||
lead.firstName.includes(campaignSearchSentence) ||
lead.lastName.includes(campaignSearch) ||
lead.lastName.includes(campaignSearchLower) ||
lead.lastName.includes(campaignSearchUpper) ||
lead.lastName.includes(campaignSearchSentence) ||
lead.email.includes(campaignSearch) ||
lead.email.includes(campaignSearchLower) ||
lead.email.includes(campaignSearchUpper) ||
lead.email.includes(campaignSearchSentence) ||
lead.phoneNo.includes(campaignSearch) ||
lead.phoneNo.includes(campaignSearchLower) ||
lead.phoneNo.includes(campaignSearchUpper) ||
lead.phoneNo.includes(campaignSearchSentence)
)
);
setCampaigns(results);
};
React.useEffect(() => {
// fetchCampaigns
(async () => {
dispatch(showTopLoader());
try {
const res = await getAgentCampaigns(authToken, "accepted");
setCampaigns(res.data.campaigns);
let leads: any[] = [];
const fetchCampaignLeads = async (id: string) => {
try {
const res = await getCampaignLeads(authToken, id);
return res.data.campaignLeads;
} catch (error) {}
};
// loop through campaigns and get leads
let resS: any[] = [];
campaigns.forEach((campaign: any, i: number) => {
const id = campaign?.Campaign?.id;
fetchCampaignLeads(id)
.then((leadsRes) => {
leads.push(leadsRes[i]);
if (id === leadsRes[i]?.campaignId)
return resS.push({
...campaign,
leads: leadsRes,
});
return (resS = campaigns);
})
.catch(() => {})
.finally(() => {
console.log(resS);
setCampaigns(resS);
});
});
} catch (error) {
} finally {
dispatch(hideTopLoader());
}
})();
}, []);
React.useEffect(() => {
setCampaigns(stateCampaigns);
campaigns.length > 0 && setShowCampaigns(true);
console.log(showCampaigns);
dispatch(hideTopLoader());
}, []);
return (
<div className={styles.wrappers}>
{/* Multi step form select a campaign first then fill info on the next step */}
<SectionHeader text="Campaign Leads" />
{showCampaigns && stateCampaigns.length === 0 && (
<>
<Empty description="No agents yet" />
<p>Join a campaign.</p>
</>
)}
{showCampaigns && stateCampaigns.length > 0 && (
<>
<p className="text-grey-500">Create Leads for Campaigns.</p>
<section className={styles.container}>
<SearchBar
placeholder="Find Campaign Leads"
onChange={handleChange}
/>
{campaigns.map((item: any) => (
<CampaignSection
key={item?.Campaign?.id}
id={item?.Campaign?.id}
name={item?.Campaign?.name}
imageUrl={item?.Campaign?.iconUrl}
campaignType={item?.Campaign?.type}
productType={item?.Campaign?.Products[0]?.type}
/>
))}
</section>
</>
)}
</div>
);
};
export default CreateLeadsCardsWrapper;

there are two things wrong in yoour code :
1- you should not have two useeffects with the same dependencies in your case: [] you have to merge those useeffects into one or change the second one's dependencies
2- doing async code in useeffect can be problematic sometimes. it is better to create an async function which does the query to the backend and sets the state and the call the function in your useeffect like below :
const getData = async()=>{
// do some queries and set the state
}
React.useeffect(()=>{
getData()
},[])

Related

Render a component only when a state array is not empty (React Router 6)

I am dealing with search parameters and redirecting of URLs in a not so pretty way (only way I could come up with). And, because of the first useEffect right under handleSubmit(), there are way too many unnecessary renders of Search component. For instance, when the page is refreshed on the search page, the Search component gets rendered 7 times (5 renders of allImages being empty, 2 renders of allImages filled with fetched images).
So, I am thinking of adding a conditional for Search component to render Search component only when allImages is not empty (when it is filled with fetched images). Let me know if this is doable.
import React from 'react'
import Navbar from './Navbar'
import create from 'zustand'
import ErrorMsg, { useError } from './ErrorMsg'
import { useEffect } from 'react'
import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'
// Zustand
let store = (set) => ({
input: '',
setInput: (value) => set({ input: value }),
allImages: [],
setAllImages: (images) => set({ allImages: images}),
totalResults: null,
setTotalResults: (num) => set({ totalResults: num}),
})
export const useHeader = create(store)
function Header() {
// global state and search params
let navigate = useNavigate()
const location = useLocation()
const [searchParams] = useSearchParams()
const query = searchParams.get('query')
const page = Number(searchParams.get('page') || 1)
const input = useHeader(state => state.input)
const setInput = useHeader(state => state.setInput)
const allImages = useHeader(state => state.allImages)
const setAllImages = useHeader(state => state.setAllImages)
const setTotalResults = useHeader(state => state.setTotalResults)
const error = useError(state => state.error)
const setError = useError(state => state.setError)
const showError = useError(state => state.showError)
const setShowError = useError(state => state.setShowError)
const setFadeOut = useError(state => state.setFadeOut)
function handleChange(event) {
setInput(event.target.value)
}
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${input}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total !== 0) {
setAllImages(data.results)
setTotalResults(data.total)
} else {
setAllImages([])
setTotalResults(0)
}
} catch(error) {
setError(error)
}
}
const handleSubmit = async (event) => {
event.preventDefault()
navigate(`/search?query=${input}&page=1`)
}
// this useEffect causes Search.js to render too many times
// especially the second conditional need improvement
useEffect(() => {
if (location.pathname === '/search' && allImages.length === 0) {
if (query) {
setInput(query)
}
navigate(`/search?query=${input}&page=${page}`)
fetchImages()
}
// need this to deal with page not refreshing when submitting or changing pages
if (location.pathname === '/search' && allImages.length !== 0) {
fetchImages()
}
// eslint-disable-next-line
}, [searchParams])
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error, setFadeOut, setShowError])
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={handleChange}
name='input'
value={input}
/>
</form>
</div>
{showError && <ErrorMsg />}
</div>
)
}
export default Header
import React from 'react'
import Header from '../Header'
import Image from '../Image'
import { useHeader } from '../Header';
import { useSearchParams } from 'react-router-dom';
function Search() {
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1)
const allImages = useHeader(state => state.allImages)
const totalResults = useHeader(state => state.totalResults)
console.log(allImages)
console.log('Search.js rendered')
// pages
function handlePrev() {
setSearchParams(params => {
params.set("page", Math.max(1, page - 1))
return params
})
}
function handleNext() {
setSearchParams(params => {
params.set("page", page + 1)
return params
})
}
return (
<div>
<Header />
{/* {totalResults === 0 && <p>Nothing Found</p>} */}
<div className='image-list mt-5 pb-5'>
{allImages.map(el => (
<Image
key={el.id}
// do need spread operator below for img's src to work in Image.js
{...el}
el={el}
/>
))}
</div>
{allImages.length !== 0 && <div className='pagination'>
<button disabled={page === 1} onClick={handlePrev}>
Prev
</button>
<h5 className='pagination--h5'>{page}</h5>
<button disabled={totalResults < 31} onClick={handleNext}>
Next
</button>
</div>}
</div>
)
}
export default Search
Finally figured it out and all I had to do was to improve the fetchImages function and simplify the useEffect.
import React from 'react'
import Navbar from './Navbar'
import create from 'zustand'
import ErrorMsg, { useError } from './ErrorMsg'
import { useEffect, useRef } from 'react'
import { useNavigate, useSearchParams } from 'react-router-dom'
// Zustand
let store = (set) => ({
input: '',
setInput: (value) => set({ input: value }),
allImages: [],
setAllImages: (images) => set({ allImages: images}),
totalResults: null,
setTotalResults: (num) => set({ totalResults: num}),
})
export const useHeader = create(store)
function Header() {
// global state and search params, and some others
let navigate = useNavigate()
const inputRef = useRef(null)
const [searchParams] = useSearchParams()
const query = searchParams.get('query')
const page = Number(searchParams.get('page') || 1)
const input = useHeader(state => state.input)
const setInput = useHeader(state => state.setInput)
const setAllImages = useHeader(state => state.setAllImages)
const setTotalResults = useHeader(state => state.setTotalResults)
const error = useError(state => state.error)
const setError = useError(state => state.setError)
const showError = useError(state => state.showError)
const setShowError = useError(state => state.setShowError)
const setFadeOut = useError(state => state.setFadeOut)
function handleChange(event) {
setInput(event.target.value)
}
const handleSubmit = async (event) => {
event.preventDefault()
navigate(`/search?query=${input}&page=1`)
}
let realShit
if (input === '') {
realShit = query
} else {
realShit = input
}
useEffect(() => {
async function fetchImages() {
try {
const res = await fetch(`https://api.unsplash.com/search/photos?&page=${page}&per_page=30&query=${realShit}&client_id=${process.env.REACT_APP_UNSPLASH_API_KEY}`)
const data = await res.json()
if (data.total === 0) {
setTotalResults(0)
} else {
setAllImages(data.results)
setTotalResults(data.total)
}
} catch(error) {
setError(error)
}
}
fetchImages()
// eslint-disable-next-line
}, [searchParams])
// input
useEffect(() => {
inputRef.current.focus()
}, [])
// error
useEffect(() => {
if (error) {
setShowError(true)
setTimeout(() => {
setFadeOut(true)
setTimeout(() => {
setShowError(false)
}, 1000)
}, 5000)
}
}, [error, setFadeOut, setShowError])
return (
<div className='header'>
<Navbar />
<h2 className='header--heading text-center text-light'>Find Images</h2>
<div className='header--form'>
<form onSubmit={handleSubmit}>
<input
className='header--form--input'
autoComplete='off'
type='text'
placeholder='Search'
onChange={handleChange}
name='input'
value={input}
ref={inputRef}
/>
</form>
</div>
{showError && <ErrorMsg />}
</div>
)
}
export default Header

React - how can I make the return of JSX wait until my useEffect() ended [duplicate]

I have fetch method in useEffect hook:
export const CardDetails = () => {
const [ card, getCardDetails ] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => getCardDetails(data))
}, [id])
return (
<DetailsRow data={card} />
)
}
But then inside DetailsRow component this data is not defined, which means that I render this component before data is fetched. How to solve it properly?
Just don't render it when the data is undefined:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data));
}, [id]);
if (card === undefined) {
return <>Still loading...</>;
}
return <DetailsRow data={card} />;
};
There are 3 ways to not render component if there aren't any data yet.
{data && <Component data={data} />}
Check if(!data) { return null } before render. This method will prevent All component render until there aren't any data.
Use some <Loading /> component and ternar operator inside JSX. In this case you will be able to render all another parts of component which are not needed data -> {data ? <Component data={data} /> : <Loading>}
If you want to display some default data for user instead of a loading spinner while waiting for server data. Here is a code of a react hook which can fetch data before redering.
import { useEffect, useState } from "react"
var receivedData: any = null
type Listener = (state: boolean, data: any) => void
export type Fetcher = () => Promise<any>
type TopFetch = [
loadingStatus: boolean,
data: any,
]
type AddListener = (cb: Listener) => number
type RemoveListener = (id: number) => void
interface ReturnFromTopFetch {
addListener: AddListener,
removeListener: RemoveListener
}
type StartTopFetch = (fetcher: Fetcher) => ReturnFromTopFetch
export const startTopFetch = function (fetcher: Fetcher) {
let receivedData: any = null
let listener: Listener[] = []
function addListener(cb: Listener): number {
if (receivedData) {
cb(false, receivedData)
return 0
}
else {
listener.push(cb)
console.log("listenre:", listener)
return listener.length - 1
}
}
function removeListener(id: number) {
console.log("before remove listener: ", id)
if (id && id >= 0 && id < listener.length) {
listener.splice(id, 1)
}
}
let res = fetcher()
if (typeof res.then === "undefined") {
receivedData = res
}
else {
fetcher().then(
(data: any) => {
receivedData = data
},
).finally(() => {
listener.forEach((cb) => cb(false, receivedData))
})
}
return { addListener, removeListener }
} as StartTopFetch
export const useTopFetch = (listener: ReturnFromTopFetch): TopFetch => {
const [loadingStatus, setLoadingStatus] = useState(true)
useEffect(() => {
const id = listener.addListener((v: boolean, data: any) => {
setLoadingStatus(v)
receivedData = data
})
console.log("add listener")
return () => listener.removeListener(id)
}, [listener])
return [loadingStatus, receivedData]
}
This is what myself needed and couldn't find some simple library so I took some time to code one. it works great and here is a demo:
import { startTopFetch, useTopFetch } from "./topFetch";
// a fakeFetch
const fakeFetch = async () => {
const p = new Promise<object>((resolve, reject) => {
setTimeout(() => {
resolve({ value: "Data from the server" })
}, 1000)
})
return p
}
//Usage: call startTopFetch before your component function and pass a callback function, callback function type: ()=>Promise<any>
const myTopFetch = startTopFetch(fakeFetch)
export const Demo = () => {
const defaultData = { value: "Default Data" }
//In your component , call useTopFetch and pass the return value from startTopFetch.
const [isloading, dataFromServer] = useTopFetch(myTopFetch)
return <>
{isloading ? (
<div>{defaultData.value}</div>
) : (
<div>{dataFromServer.value}</div>
)}
</>
}
Try this:
export const CardDetails = () => {
const [card, setCard] = useState();
const { id } = useParams();
useEffect(() => {
if (!data) {
fetch(`http://localhost:3001/cards/${id}`)
.then((res) => res.json())
.then((data) => setCard(data))
}
}, [id, data]);
return (
<div>
{data && <DetailsRow data={card} />}
{!data && <p>loading...</p>}
</div>
);
};

Filter array and show results in React

I am trying to filter my results via button onClicks in React, but for some reason its not working?
https://stackblitz.com/edit/react-x7tlxr?file=src/App.js
import React, {useEffect, useState} from "react";
import axios from 'axios';
import "./style.css";
export default function App() {
const [fetchData, setFetchData] = useState([]);
const [loading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const url = 'https://jsonplaceholder.typicode.com/todos';
useEffect(() => {
let mounted = true;
const loadData = async () => {
try {
const response = await axios(url);
if (mounted) {
setFetchData(response.data);
setLoading(false);
setIsError(false);
console.log('data mounted')
}
} catch (err) {
setIsError(true)
setLoading(false);
setFetchData([]);
console.log(err);
}
};
loadData();
return () => {
mounted = false;
console.log('cleaned');
};
},
[url]
);
const renderdata = () => {
if (fetchData) {
return (
<div>{fetchData.map(inner => {
return (
<React.Fragment key={inner.id}>
<p>{inner.title}</p>
</React.Fragment>
)
})}</div>
)
} else {
<p>No data to display!</p>
}
}
const handle1 = () => {
const result1 = fetchData.filter((inner) => inner.id === '1');
setFetchData(result1);
}
const handle2 = () => {
const result2 = fetchData.filter((inner) => inner.id === '2');
setFetchData(result2);
}
const handleAll = () => {
setFetchData(fetchData);
}
return (
<div>
<button onClick={handleAll}>Show all</button>
<button onClick={handle1}>Filter by id 1</button>
<button onClick={handle2}>Filter by id 2</button>
{renderdata()}
</div>
);
}
The id is a number, not a string, so you filter(s) need to be changed like that:
const result1 = fetchData.filter((inner) => inner.id === 1);
You have another problem, is that you change the whole data set when you filter, so once you've filtered, you can't "unfilter" or filter again on another id.
You need to maintain the original fetchedData unchanged.
This example shows how it can be fixed.
I found 2 issues in your code
Id in the API result is numeric not string
inner.id === '2'. so this will return false inner.id === 2 you need to use like this
when you assign the filtered value to the original array it will of course change the original array so when you try to filter it second time you don't have the original API result in the fetch data array because it is already filtered
So i have created one more array filteredData
This will work.
import React, {useEffect, useState} from "react";
import axios from 'axios';
import "./style.css";
export default function App() {
const [fetchData, setFetchData] = useState([]);
const [filteredData, setFileredData] = useState([]);
const [loading, setLoading] = useState(true);
const [isError, setIsError] = useState(false);
const url = 'https://jsonplaceholder.typicode.com/todos';
useEffect(() => {
let mounted = true;
const loadData = async () => {
try {
const response = await axios(url);
if (mounted) {
setFetchData(response.data);
setFileredData(response.data)
setLoading(false);
setIsError(false);
console.log('data mounted')
}
} catch (err) {
setIsError(true)
setLoading(false);
setFetchData([]);
console.log(err);
}
};
loadData();
return () => {
mounted = false;
console.log('cleaned');
};
},
[url]
);
const renderdata = () => {
if (filteredData) {
return (
<div>{filteredData.map(inner => {
return (
<React.Fragment key={inner.id}>
<p>{inner.title}</p>
</React.Fragment>
)
})}</div>
)
} else {
<p>No data to display!</p>
}
}
const handle1 = () => {
const result1 = fetchData.filter((inner) => inner.id === 1);
setFileredData(result1);
}
const handle2 = () => {
const result2 = fetchData.filter((inner) => inner.id === 2);
setFileredData(result2);
}
const handleAll = () => {
setFileredData(fetchData);
}
return (
<div>
<button onClick={handleAll}>Show all</button>
<button onClick={handle1}>Filter by id 1</button>
<button onClick={handle2}>Filter by id 2</button>
{renderdata()}
</div>
);
}

converting class to hooks getting messages

i'm new to react hooks, here i have been converting my project to hooks from classes, i'm getting this kind of message 'Error: Server error
at build_error (actions.js:57)
at eval (actions.js:83)' and 'GET http://127.0.0.1:8000/api/kamera/undefined 404 (Not Found)'
those errors come when i'm changing class to hooks (everything is set correcly using useState and useEffect), any idea ?
class:
initializeCollapses() {
const data = this.props[this.props.action];
let collapseStates = this.state.collapseStates;
if (!data || data.length < 1) {
return;
}
data.map((el) => {
collapseStates["" + el.name + el.identifier] = false;
return;
});
this.setState({
...this.state,
collapseStates: collapseStates,
});
}
componentDidMount() {
this.props.getItems[this.props.action](this.state.actionArgs).then(() => {
this.initializeCollapses();
});
}
Hooks:
const initializeCollapses = () => {
const data = [action];
if (!data || data.length < 1) {
return;
}
data.map((el) => {
collapseStates["" + el.name + el.identifier] = false;
return;
});
setCollapseStates(collapseStates);
};
useEffect(() => {
getItems[action](actionArgs).then(() => {
initializeCollapses();
});
}, []);
initializeCollapses() {
const data = this.props[this.props.action];
let collapseStates = this.state.collapseStates;
if (!data || data.length < 1) {
return;
}
data.map((el) => {
collapseStates["" + el.name + el.identifier] = false;
return;
});
this.setState({
...this.state,
collapseStates: collapseStates,
});
}
componentDidMount() {
this.props.getItems[this.props.action](this.state.actionArgs).then(() => {
this.initializeCollapses();
});
}
const mapDispatchToProps = (dispatch) => {
return {
getItems: {
analysers: (site) => dispatch(getAnalysers(site)),
platforms: (site) => dispatch(getPlatforms(site)),
brokers: (site) => dispatch(getBrokers(site)),
cameras: (site) => dispatch(getCameras(site)),
sites: (site) => dispatch(getSites())
},
};
};
The above class implementation in hooks would roughly be as below
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import getItems from "./store/actions";
or
import { cameras, sites, platform, brokers } from "./store/actions";
const actionArgs = useSelector(state => state.actionArgs); // In place of mapStateToProps
const dispatch = useDispatch();
useEffect(() => {
dispatch(getItems.cameras(actionArgs)) or dispatch(cameras(actionArgs)) //If destructured
}, []);
I have provided an understandable example with whatever data you provided. Refer this for a completely different approach or this one for the same mapDispatchToProps approach.
Good to refer
Example:
import React, {useReducer} from 'react';
const init = 0;
const myReducer = (state, action) => {
switch(action.type){
case 'increment':
return state + 1 // complex actions are kept in seperate files for better organised, clean code
case 'decrement':
return state - 1
case 'reset': // action types as well are kept as selectors
return init
default:
return state
}
};
function ReducerExample(){
const [count, dispatch] = useReducer(myReducer, init)
const add = () => {
dispatch({type: 'increment'})
}
const sub = () => {
dispatch({type: 'decrement'})
}
const reset = () => {
dispatch({type: 'reset'})
}
return (
<div>
<h4>Count: {count}</h4>
<button onClick={add} style={{margin: '10px'}}>Increment</button>
<button onClick={sub}>Decrement</button>
<button onClick={reset} style={{margin: '10px'}}>Reset</button>
</div>
)
}
export default ReducerExample;

Browser tab freezes after using useState hook in React

I have a blog app and it worked perfectly before I have added the user login feature. After that useState hook methods freeze the application tab in the browser. I am not sure what the problem is, I am guessing it has something to do with re-rendering.
Here is my App.js
import React, { useState, useEffect } from 'react'
import Header from './components/Header'
import Filter from './components/Filter'
import AddNewBlog from './components/AddNewBlog'
import Blogs from './components/Blogs'
import blogService from './services/blogs'
import Notification from './components/Notification'
import Button from './components/Button'
import LoginForm from './components/LoginForm'
import loginService from './services/login'
import './App.css'
const App = () => {
const [ blogs, setBlogs] = useState([])
const [ newTitle, setNewTitle ] = useState('')
const [ newAuthor, setNewAuthor ] = useState('')
const [ newUrl, setNewUrl ] = useState('')
const [ newLike, setNewLike ] = useState('')
const [ blogsToShow, setBlogsToShow] = useState(blogs)
const [ message, setMessage] = useState(null)
const [ notClass, setNotClass] = useState(null)
const [ username, setUsername ] = useState('')
const [ password, setPassword ] = useState('')
const [ user, setUser ] = useState(null)
useEffect(() => {
blogService
.getAll()
.then(initialBlogs => {
setBlogs(initialBlogs)
console.log(initialBlogs)
setBlogsToShow(initialBlogs)
})
.catch(error => {
showMessage(`Error caught: ${error}`, 'error')
})
}, [])
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
})
const handleLogin = async (e) => {
e.preventDefault()
try {
const user = await loginService.login({
username, password,
})
window.localStorage.setItem('loggedBlogappUser', JSON.stringify(user))
blogService.setToken(user.token)
setUser(user)
setUsername('')
setPassword('')
} catch (error) {
showMessage('wrong credentials', 'error')
}
}
const handleLogout = () => {
console.log('logging out')
setUser(null)
window.localStorage.clear()
}
const handleAddClick = (e) => {
e.preventDefault()
if(newTitle === '') {
alert("Input Title")
}
else if (newAuthor === '') {
alert("Input Author")
}
else if (newUrl === '') {
alert("Input Url")
} else {
let newObject = {
title: newTitle,
author: newAuthor,
url: newUrl,
likes: 0
}
console.log('step0');
blogService
.create(newObject)
.then(returnedBlog => {
setBlogs(blogs.concat(returnedBlog))
setBlogsToShow(blogs.concat(returnedBlog))
resetForm()
showMessage(`Added ${newTitle}`, 'success')
})
.catch(error => {
console.log(error.response.data)
showMessage(`${error.response.data.error}`, 'error')
})
//}
}
}
const handleDeleteClick = (id, title) => {
let message = `Do you really want to delete ${title}?`
if(window.confirm(message)){
blogService
.deleteBlog(id)
.then(res => {
setBlogs(blogs.filter(b => b.id !== id))
setBlogsToShow(blogs.filter(b => b.id !== id))
})
.catch(error => {
showMessage(`${title} has already been removed from the server`, 'error')
})
}
}
const handleLikeClick = (blog) => {
const updatedObject = {
...blog,
likes: blog.likes += 1
}
blogService
.update(updatedObject)
.then(() => {
setBlogs(blogs)
showMessage(`You liked ${updatedObject.title}`, 'success')
})
}
const resetForm = () => {
setNewTitle('')
setNewAuthor('')
setNewUrl('')
setNewLike('')
document.getElementById('titleInput0').value = ''
document.getElementById('authorInput0').value = ''
document.getElementById('urlInput0').value = ''
}
const showMessage = (msg, msgClass) => {
setMessage(msg)
setNotClass(msgClass)
setTimeout(() => {
setMessage(null)
setNotClass(null)
}, 5000)
}
const handleFilterOnChange = (e) => {
const filtered = blogs.filter(blog => blog.title.toLowerCase().includes(e.target.value.toLowerCase()))
setBlogsToShow(filtered)
//setBlogs(filtered)
}
const handleAddTitleOnChange = (e) => {
console.log(e.target.value)
console.log(newTitle)
setNewTitle(e.target.value)
}
const handleAddAuthorOnChange = (e) => {
setNewAuthor(e.target.value)
}
const handleAddUrlOnChange = (e) => {
setNewUrl(e.target.value)
}
if (user === null) {
return (
<div>
<Header text={'Bloglist'} />
<Notification message={message} notClassName={notClass} />
<LoginForm
handleLogin={handleLogin}
username={username}
setUsername={setUsername}
password={password}
setPassword={setPassword}
/>
</div>
)
}
return (
<div>
<Header text={'Bloglist'} />
<Notification message={message} notClassName={notClass} />
<p>{user.name} logged in</p><Button text={"logout"} handleClick={handleLogout} />
<AddNewBlog
handleAddTitleOnChange={handleAddTitleOnChange}
handleAddAuthorOnChange={handleAddAuthorOnChange}
handleAddUrlOnChange={handleAddUrlOnChange}
handleAddClick={handleAddClick}
/>
<Filter handleFilterOnChange={handleFilterOnChange} />
<Blogs blogs={blogsToShow} handleDeleteClick={handleDeleteClick} handleLikeClick={handleLikeClick} />
</div>
)
}
export default App
Anytime I call anyone of these methods: "setNewTitle, setNewAuthor, setNewUrl, setBlogsToShow", after logging in, the tab of the browser freezes. I tried with Chrome and FireFox.
Thank you for your help.
The issue is with your useEffect
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
})
It is executed on each re-render since it has not been provided any dependency and so it send the app in an infinite loop as it itself triggers a re-render. So when you call any state updater, this useEffect is triggered causing you tab to freeze
You can make this useEffect run once on initial render by passing an empty array to it as dependency
useEffect(() => {
const loggedUserJSON = window.localStorage.getItem('loggedBlogappUser')
if (loggedUserJSON) {
const user = JSON.parse(loggedUserJSON)
setUser(user)
blogService.setToken(user.token)
}
}, [])

Categories

Resources