About route(link) and asynchronous fetch data in react - javascript

First of all, good evening. I'm trying to improve myself at React. So I'm working on a Starwars project ๐Ÿ‘จโ€๐Ÿ’ป.
I have two problems.
First of all, I listed different characters at the bottom of my character detail page. Again, I want it to be directed to different characters through the same component. But even if the link changes, my component is not refreshed. But the picture is changing ๐Ÿค”.
Note:
sandbox link : https://codesandbox.io/s/github/kasim444/Javascript-Camp-2019/tree/master/challenges/star-wars-app/
my project github link : https://github.com/kasim444/Javascript-Camp-2019/tree/master/challenges/star-wars-app/
// component that I redirect in.
class CharacterDetail extends Component {
render () {
const characterId = this.props.match.params.id;
const {
name,
height,
mass,
hair_color,
skin_color,
eye_color,
birthday_year,
gender,
homeworld,
loading,
} = this.state;
return loading
? <Loading />
: (
<div>
<main className="characterBg">
<DetailHeader imgLink={characterAvatarLink[characterId - 1]} />
<CharacterContent
imgLink={characterAvatarLink[characterId- 1]}
characterInfo={this.state}
/>
</main>
<FeaturedCharacters />
</div>
);
}
}
// feautered character component
function FeaturedCharacters () {
const [characters, setCharacters] = useState ([]);
const [loading, setLoading] = useState (true);
const fetchCharacters = async () => {
const data = await fetch ('https://swapi.co/api/people/');
const fetchPeople = await data.json ();
const feauteredCharacter = fetchPeople.results.filter (
(character, index) => index < 4
);
setCharacters (feauteredCharacter);
setLoading (false);
};
useEffect (() => {
fetchCharacters ();
}, []);
return (
<div>
<h2>Popular Characters</h2>
<div className="d-flex-row container">
{loading
? <PlaceholderDiv />
: characters.map ((character, index) => (
<CharacterCard
key={character.name}
chaId={index + 1}
chaDet={character.name}
imgLink={characterAvatarLink[index]}
/>
))}
</div>
</div>
);
}
// character card link component
const CharacterCard = props => {
const name = props.chaDet;
return (
<Link className="profile_card" to={`/character/${props.chaId}`}>
<div className="profile_image">
<img src={props.imgLink} alt={name} />
</div>
<div className="profile_content">
<h3>{name}</h3>
<div className="read_more d-flex-row">
<img src={showIcon} alt="Show Icon" />
Show More
</div>
</div>
</Link>
);
};
// main component
const App = () => {
return (
<Router>
<div className="st-container d-flex-column">
<Header />
<Switch>
<Route exact path="/" component={HomePage} />
<Route path="/movie/:title" component={MovieDetails} />
<Route path="/character/:id" component={CharacterDetail} />
<Route
path="/githubProfile"
component={() => {
window.location.href = 'https://github.com/kasim444/Javascript-Camp-2019/tree/master/challenges/star-wars-app';
return null;
}}
/>
</Switch>
<Footer />
</div>
</Router>
);
};
My second problem is that I draw a data back from the data from Api. I can reach outlines of the character. It's working now. But I feel that there are some things that don't feel right.๐Ÿคจ How can I improve Fetch operations in Axios?
async componentDidMount () {
const characterId = this.props.match.params.id;
const filmSeries = [];
const characterDetail = await axios.get (
`https://swapi.co/api/people/${characterId}/`
);
const filmsFetchLinks = characterDetail.data.films;
const promisesData = await filmsFetchLinks.map(link => axios.get(link));
axios.all (promisesData).then(value => {
value.map (val => filmSeries.push (val.data.title));
let {
name,
height,
mass,
hair_color,
skin_color,
eye_color,
birthday_year,
gender,
homeworld,
films,
} = characterDetail.data;
fetch(homeworld).then(home => home.json()).then(val => this.setState({homeworld: val.name}));
this.setState ({
name,
height,
mass,
hair_color,
skin_color,
eye_color,
birthday_year,
gender,
films: filmSeries,
loading: false,
});
});
}
I'm sorry if I bored you. It seems a little long because the components are interconnected. Thank you in advance for your interest. ๐Ÿ–– ๐Ÿ™

You can use componentDidUpdate and compare the the current parameter id to the previous one (you would have to save it to state) and you fetch data again IFF the two are different. componentDidUpdate will go every time the route changes.
A better approach would be to use useEffect and depend on the parameter id. In the use effect, you do all your data fetching. Something like this:
const { id: characterId } = props.match.params;
React.useEffect(() => {
async function getData() {
// fetch all you data here
}
getData();
}, [characterId]);
You can see a crude version of this here:
https://codesandbox.io/s/star-wars-app-sx8hk

Related

React-Router - How to show data passed from one component to another using useNavigate or Link and useLocation

I'm basically trying to show some data in one of my components. The data is passed from my main page but I can't get it to work using useLocation.
I'm getting the data from my firebase db.
My main page is a job board and I want users to be able to click on the job card and go to a new page with all the details of that job.
I see on the console that I get the data but I can't seem to display it in my component/page. I get undefined when doing console.log
See below for more details:
Jobs.js
import { Link, useNavigate } from "react-router-dom";
export default () => {
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
return (
<div>
{jobs.map(job => {
return (
<div key={job.id}>
//My attempt using Link
<Link
to={`/view-contact-details/${job.id}`}
state={{jobs}}
>
<button>View</button>
</Link>
//My attempt using useNavigate
<button onClick={() => {
navigate(`/view-contact-details/${job.id}`, { state:{ jobs } });
}}
>
Go To Job details
</button>
</div>
);
})}
</div>
);
};
App.js
import React from "react";
import Jobs from "./pages/jobs"
import Form from "./pages/form"
import { Routes, Route } from "react-router-dom"
import ViewUserDetails from "./components/Job/ViewJobDetails";
export default () => {
return (
<div className="App">
<Routes>
<Route exact path='/' element={<Jobs/>} />
<Route exact path='/form' element={<Form/>} />
<Route
exact
path="/view-contact-details/:id"
element={<ViewJobDetails/>}
/>
</Routes>
</div>
);
};
ViewJobDetails.js
import React from "react";
import { useLocation, } from "react-router-dom";
export default (props) => {
const location = useLocation();
console.log(location); // shows jobs on the page yet can't display
//I also tried
//const {state} = location
//{state.job.description}
return (
<>
<div>
<div>
<div>
<strong>Description:</strong>
{location.state.job.description} //doesn't work
{location.job.description} //doesn't work
{location.state.description} //doesn't work
</div>
<div>
<strong>Title:</strong>
</div>
</div>
</div>
</>
);
};
console.log output
The passed state jobs is an array. In the component you are accessing a job property that is undefined.
Access the correct state.jobs array and map it to JSX.
Example:
export default (props) => {
const { state } = useLocation();
const { jobs } = state || []; // <-- access state.jobs
return (
<div>
{jobs.map(job => ( // <-- map jobs array
<React.Fragment key={job.id}>
<div>
<strong>Description:</strong>
{job.description}
</div>
<div>
<strong>Title:</strong>
{job.title}
</div>
</React.Fragment>
))}
</div>
);
};
If on the offhand chance you meant to pass only a single job, i.e. the current job from Jobs, then instead of passing the entire array, pass only the currently iterated job object.
export default () => {
const navigate = useNavigate();
const [jobs, setJobs] = useState([]);
return (
<div>
{jobs.map(job => {
return (
<div key={job.id}>
//My attempt using Link
<Link
to={`/view-contact-details/${job.id}`}
state={{ job }} // <-- pass current job
>
<button>View</button>
</Link>
//My attempt using useNavigate
<button
onClick={() => {
navigate(
`/view-contact-details/${job.id}`,
{ state:{ job } } // <-- pass current job
);
}}
>
Go To Job details
</button>
</div>
);
})}
</div>
);
};
Then in the JobDetals component access location.state.job.
export default (props) => {
const { state } = useLocation();
const { job } = state || {};
return (
<>
<div>
<div>
<div>
<strong>Description:</strong>
{job.description}
</div>
<div>
<strong>Title:</strong>
{job.title}
</div>
</div>
</div>
</>
);
};

React not updates component on store changes

I have component with cards. Needs to update it on page changing with Redux store. I understand why code is not working but have no idea how to fix it.
function Cards(props) {
const { beers = [] } = props
const changePage = (newPage) => {
page = newPage
}
let page = store.getState().pages
store.subscribe(() => changePage(store.getState().pages))
return (
<div className='cards'>
<div className='cards__content'>
{beers.slice(page*9, page*9+9).map(beer => <MenuCard name={beer.name} description={beer.brewers_tips} id={beer.id} key={beer.id} />)}
</div>
<Pages amount={Math.floor(beers.length / 9)} />
</div>
)
}

React: image src is not taking path via API. image is undefined

I'm new to Reactjs. I'm unable to extract image url from the API. I'm really sorry if similar type of threads already exist in stackoverflow. Full code is below.
The API I used is: https://api.thecatapi.com/v1/breeds
import React, { Component } from "react";
import ReactDOM from "react-dom";
const Cat = ({
cat: {name, image},
}) => {
return(
<div className='catMain'>
<div className='catImage'>
<img src={image.url} alt={name} />
</div>
</div>
)
}
class App extends Component {
state = {
data: [],
};
componentDidMount() {
this.fetchCatData();
}
fetchCatData = async () => {
const url1 = "https://api.thecatapi.com/v1/breeds"
const response = await fetch(url1)
const data = await response.json()
this.setState({
data,
})
}
render() {
return (
<div className='main'>
<div className="cats">
<div className="catsInfo">
{this.state.data.map((cat) => (
<Cat cat={cat}/>
))}
</div>
</div>
</div>
)
}
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
If you want to display every cat even though they might not have an image, you can do this:
const Cat = ({ cat: { name, image } }) => {
return (
<div className="catMain">
{image && (
<div className="catImage">
<img src={image.url} alt={name} />
</div>
)}
</div>
);
};
Then you can just display whatever else you want about the cat, without having to worry about image issues.
You have an assumption that every cat has an image, that's not the case, therefore you can filter empty images beforehand (there are tons of solutions, conditional rendering can be done too):
// cat.image is an object, which is truthy value therefore you can filter by it.
this.state.data.filter(cat => cat.image).map(cat => <Cat cat={cat} />)
// OR, conditional rendering
this.state.data.map(cat => cat.image && <Cat cat={cat} />)
// Or in the component itself, etc.
const Cat = ({ cat: { name, image } }) => {
return image ? (
<div className="catMain">
<div className="catImage">
<img src={image.url} alt={name} />
</div>
</div>
) : (
<Placeholder />
);
};

How to refactor an if else if with previous state when using useState Hook?

I have 2 details tag, each has a control to toggle it on/off. Code snippet here. Clicking Control A should toggle on/off page A, clicking Control B should toggle on/off page B.
I did it with an if else if plus 2 useState, this would not be feasible when there are multiple details. How can I refactor the code such that maybe the if else if can be avoided and it detects which Control I click in a cleverer way?
Page.js
const Page = ({ name, isOpen, setIsOpen }) => {
return (
<>
<details
open={isOpen}
onToggle={(e) => {
setIsOpen(e.target.open);
}}
>
<summary>Page {name} title</summary>
<div>Page {name} contents</div>
</details>
</>
);
};
export default Page;
Control.js
const Control = ({ toggle }) => {
return (
<>
<a onClick={() => toggle("A")} href="#/">
Control A
</a>
<br />
<a onClick={() => toggle("B")} href="#/">
Control B
</a>
</>
);
};
App.js
export default function App() {
const [isOpenA, setIsOpenA] = useState(false);
const [isOpenB, setIsOpenB] = useState(false);
const toggle = (name) => {
if (name === "A") {
setIsOpenA((prevState) => !prevState);
} else if (name === "B") {
setIsOpenB((prevState) => !prevState);
}
};
return (
<div className="App">
<Control toggle={toggle} />
<Page name={"A"} isOpen={isOpenA} setIsOpen={setIsOpenA} />
<Page name={"B"} isOpen={isOpenB} setIsOpen={setIsOpenB} />
</div>
);
}
You can use an array to represent open ones
const [openPages, setOpenPages] = useState([])
And to toggle filter the array
const toggle = (name) => {
if(openPages.includes(name)){
setOpenPages(openPages.filter(o=>o!=name))
}else{
setOpenPages(pages=>{ return [...pages,name]}
}
}
I would personally use an object as a map for your toggles as in something like:
const [isOpen, _setIsOpen] = useState({});
const setIsOpen = (pageName,value) => _setIsOpen({
...isOpen,
[pageName]: value
});
const toggle = (name) => setIsOpen(name, !isOpen[name]);
and then in the template part:
<Page name={"A"} isOpen={isOpen["A"]} setIsOpen={toggle("A")} />
In this way you can have as many toggles you want and use them in any way you want
I think this would be quite cleaner, also you should put the various page names in an array and iterate over them as in
const pageNames = ["A","B"];
{
pageNames.map( name =>
<Page name={name} isOpen={isOpen[name]} setIsOpen={toggle(name)} />)
}
At least that's how I would go about it
Adithya's answer worked for me.
For future reference, I put the full working code here. The onToggle attribute in Page.js is not needed. All required is passing correct true/false to open={isOpen} in Page.js.
App.js:
export default function App() {
const [openPages, setOpenPages] = useState([]);
const toggle = (name) => {
if (openPages.includes(name)) {
setOpenPages(openPages.filter((o) => o !== name));
} else {
setOpenPages((pages) => {
return [...pages, name];
});
}
};
return (
<div className="App">
<Control toggle={toggle} />
<Page name={"A"} isOpen={openPages.includes("A")} />
<Page name={"B"} isOpen={openPages.includes("B")} />
<Page name={"C"} isOpen={openPages.includes("C")} />
</div>
);
}
Page.js
const Page = ({ name, isOpen }) => {
return (
<>
<details open={isOpen}>
<summary>Page {name} title</summary>
<div>Page {name} contents</div>
</details>
</>
);
};
Control.js remains the same.

Load more data on scroll

I have a page with a search input, once the user clicks on submit the results come up.
There can be a lot of results (usually not but there can be thousands of results) and I don't want to load them all at once, how can I get a few dozens and fetch more results from he API as the user scrolls down, what's the correct way to do that? I was thinking that Lodash throttle can fit but I couldn't find a good example for it.
This is my react component:
const getContacts = async (searchString) => {
const { data: contactsInfo} = await axios.get(`api/Contats/Search?contactNum=${searchString}`);
return contactsInfo;
};
export default class Home extends React.Component {
state = {
contactsInfo: [],
searchString: '',
};
handleSubmit = async () => {
const { searchString } = this.state;
const contactsInfo = await getContacts(searchString);
this.setState({ contactsInfo });
};
onInputChange = e => {
this.setState({
searchString: e.target.value,
});
};
onMouseMove = e => {
};
render() {
const { contactsInfo, searchString, } = this.state;
return (
<div css={bodyWrap} onMouseMove={e => this.onMouseMove(e)}>
<Header appName="VERIFY" user={user} />
{user.viewApp && (
<div css={innerWrap}>
<SearchInput
searchIcon
value={searchString || ''}
onChange={e => this.onInputChange(e)}
handleSubmit={this.handleSubmit}
/>
{contactsInfo.map(info => (
<SearchResultPanel
info={info}
isAdmin={user.isAdmin}
key={info.id}
/>
))}
</div>
)}
<Footer />
</div>
);
}
}
If the API supports pagination then you can use React-Infinite-Scroll. It looks like this
<div style="height:700px;overflow:auto;">
<InfiniteScroll
pageStart={0}
loadMore={loadFunc}
hasMore={true || false}
loader={<div className="loader">Loading ...</div>}
useWindow={false}>
{items}
</InfiniteScroll>
</div>
However if the REST API does not support it, you can still load the data and show them in chunks to the user with the same library but you would need to handle the current load state by yourself.

Categories

Resources