my component App
import React, { Component } from "react";
import City from "./City";
import withDataLoader from "./withDataLoader";
const App = () => {
const MyCity = withDataLoader(
City,
"https://5e5cf5eb97d2ea0014796f01.mockapi.io/api/v1/cities/1"
);
return (
<div className="page">
<MyCity />
</div>
);
};
export default App;
my Higher-Order Components
import React from "react";
import Spinner from "./Spiner";
const withDataLoader = (url, Component) => {
class Container extends React.Component {
state = "";
componentDidMount() {
this.get();
}
get = () => {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState(data));
};
render() {
return this.state === "" ? <Spinner /> : <Component data={this.state} />;
}
};
return Container
};
export default withDataLoader;
I pass parameters to HOC
withDataLoader(url, City)
but in the documentation I saw another entry
withDataLoader(url)(City) https://ru.reactjs.org/docs/higher-order-component...
how to make it so that you can pass parameters to the hock so withDataLoader(url)(City)?
Changing from withDataLoader(url, City) to withDataLoader(url)(City) called currying.
// Change function declaration
const withDataLoader = (url, Component) => {}
|
v
const withDataLoader = (url) => (Component) => {}
And note that you might have a missplaced values in function call:
// Instead (City,URL)
const MyCity = withDataLoader("https://...",City)
I solved the problem this way
const withDataLoader = (url) => {
return function (Component) {
return class Container extends React.Component {
state = "";
componentDidMount() {
this.get();
}
get = () => {
fetch(url)
.then((response) => response.json())
.then((data) => this.setState(data));
};
render() {
return this.state === "" ? (
<Spinner />
) : (
<Component data={this.state} />
);
}
};
};
};
Related
PropTable should render only if receives props from other component
const PropTable = (props) => {
const { availableProp } = props;
...
return(...)
}
This'd get your job done
const PropTable = (props) => {
const { availableProp } = props;
...
return availableProp ? (...) : null
}
I am learning react.
I have a simple react app sample that :
Fetch users
Once users are fetched, show their name on a Card
What I'd like to do is to expand this sample. Instead of using a simple list of users, I'd like to use a list of pokemons. What I try to do is :
Fetch the list of pokemon and add in state.pokemons
Show the Card with the pokemon name from state.pokemons
From that list, get the URL to fetch the detail of the given pokemon and add in state.pokemonsDetails
From the state.pokemonsDetails, update the Cards list to show the image of the pokemon.
My problem is: I don't even know how to re-render the Cards list after a second fetch.
My question is: How to update the Cards list after the second fetch?
See my code below:
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox"
import Scroll from "../components/Scroll"
import './App.css';
class App extends React.Component{
constructor(){
super();
this.state = {
pokemons:[],
pokemonsDetails:[],
searchfield: ''
}
}
getPokemons = async function(){
const response = await fetch('https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20');
const data = await response.json();
this.setState({pokemons:data.results})
}
getPokemonDetails = async function(url){
//fetch function returns a Promise
const response = await fetch(url);
const data = await response.json();
//console.log('getPokemonDetails', data);
this.setState({pokemonsDetails:data});
}
componentDidMount(){
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({searchfield: event.target.value})
}
render(){
const {pokemons, pokemonsDetails, searchfield} = this.state;
if(pokemons.length === 0){
console.log('Loading...');
return <h1>Loading....</h1>
}else if (pokemonsDetails.length === 0){
console.log('Loading details...');
pokemons.map(pokemon => {
return this.getPokemonDetails(pokemon.url);
});
return <h1>Loading details....</h1>
}else{
return(
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange}/>
<Scroll>
<CardList pokemons={pokemons}/>
</Scroll>
</div>
);
}
}
}
export default App;
Some remarks :
I can see a problem where my Cards list is first created with state.pokemons, then, I would need to update Cards list with state.pokemonsDetails. The array is not the same.
Second problem, I don't even know how to call the render function after state.pokemonsDetails is filled with the fetch. I set the state, but it looks like render is not called every time
More a question than a remark. The way I update my state in getPokemonDetails might be incorrect. I keep only one detail for one given pokemon. How to keep a list of details? Should I use something else than setState to expand pokemonsDetails array?
You can combine 2 API calls before pokemons state update that would help you to control UI re-renderings better
You can try the below approach with some comments
Side note that I removed pokemonDetails state, so you won't see the loading elements for pokemonDetails as well
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
//try to get all pokemon details at once with fetched URLs
const pokemonDetails = await Promise.all(
data.results.map((result) => this.getPokemonDetails(result.url))
);
//map the first and second API response data by names
const mappedPokemons = pokemonDetails.map((pokemon) => {
const pokemonDetail = pokemonDetails.find(
(details) => details.name === pokemon.name
);
return { ...pokemon, ...pokemonDetail };
});
//use mapped pokemons for UI display
this.setState({ pokemons: mappedPokemons });
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
If you want to update pokemon details gradually, you can try the below approach
import React from "react";
import CardList from "../components/CardList";
import SearchBox from "../components/SearchBox";
import Scroll from "../components/Scroll";
import "./App.css";
class App extends React.Component {
constructor() {
super();
this.state = {
pokemons: [],
searchfield: ""
};
}
getPokemons = async function () {
const response = await fetch(
"https://pokeapi.co/api/v2/pokemon/?offset=0&limit=20"
);
const data = await response.json();
this.setState({ pokemons: data.results });
for (const { url } of data.results) {
this.getPokemonDetails(url).then((pokemonDetails) => {
this.setState((prevState) => ({
pokemons: prevState.pokemons.map((pokemon) =>
pokemon.name === pokemonDetails.name
? { ...pokemon, ...pokemonDetails }
: pokemon
)
}));
});
}
};
getPokemonDetails = async function (url) {
return fetch(url).then((response) => response.json());
};
componentDidMount() {
this.getPokemons();
}
onSearchChange = (event) => {
this.setState({ searchfield: event.target.value });
};
render() {
const { pokemons, searchfield } = this.state;
if (pokemons.length === 0) {
return <h1>Loading....</h1>;
} else {
return (
<div>
<h1>Pokedex</h1>
<SearchBox searchChange={this.onSearchChange} />
<Scroll>
<CardList pokemons={pokemons} />
</Scroll>
</div>
);
}
}
}
export default App;
Sandbox
Side note that this approach may cause the performance issue because it will keep hitting API for fetching pokemon details multiple times and updating on the same state for UI re-rendering
I'm stuck with a loop generated when I used async function, and I need the object inside in the Promise to use it in Child component, this is the issue:
ParentComponent:
class ParentComponent extends Component {
state={
array:[]
}
async getData() {
const {data} = await getAxiosFunction();
let content = [...data['content']]; // object structure
this.setState({
array: content,
});
}
render(){
const {array} = this.state;
console.log(this.getData()); // loop Promise {<pending>}
return(
<div>
<button onClick={e=>{e.preventDefault(); this.getData();}}>get data</button>
{array>0 &&<ChildComponent data={array} />}
</div>
)
}
}
export default ParentComponent;
ChildComponent:
const ChildComponent =({data})=>{
return(
<div>
... // I need to use the object inside data
</div>
)
}
export default ChildComponent;
I hope that some one can let me some advice to apply it, thanks!
Because you use console.log(getData()) in the render part.
The flow like :
reder => run getData => setState => render => run getData => setState ..... (infinite loop)
I fix you sample code and make it work, like :
( Or you can check here )
import React, { Component } from "react";
const ChildComponent = ({ data }) => {
return <div>{data}</div>;
};
class ParentComponent extends Component {
constructor(props) {
super(props);
this.getData = this.getData.bind(this);
this.state = {
array: []
};
}
async getData() {
// const {data} = await getAxiosFunction();
// let content = [...data['content']]; // object structure
let content = ["Hi, you click button"];
this.setState({
array: content
});
}
render() {
const { array } = this.state;
return (
<div>
<button
onClick={(e) => {
e.preventDefault();
this.getData();
}}
>
get data
</button>
{array.length > 0 && <ChildComponent data={array} />}
</div>
);
}
}
export default ParentComponent;
You can call you getData method inside componentDidMount() which will get executed only once after your component is mounted successfully.
class ParentComponent extends Component {
state={
array:[]
}
componentDidMount() {
this.getData();
}
async getData() {
const {data} = await getAxiosFunction();
let content = [...data['content']]; // object structure
this.setState({
array: content,
});
}
render(){
const {array} = this.state;
return(
<div>
<button onClick={e=>{e.preventDefault(); this.getData();}}>get data</button>
{array>0 &&<ChildComponent data={array} />}
</div>
)
}
}
export default ParentComponent;
And if you want to get the data on button click then you can do
class ParentComponent extends Component {
state={
array:[]
}
async getData() {
const {data} = await getAxiosFunction();
let content = [...data['content']]; // object structure
this.setState({
array: content,
});
}
render(){
const {array} = this.state;
return(
<div>
<button onClick={() => this.getData()}>get data</button>
{array>0 &&<ChildComponent data={array} />}
</div>
)
}
}
export default ParentComponent;
Check the sandbox with fix https://stackblitz.com/edit/react-mtd8v3
Do not call the getData in render method (console.log), which will cause infinite loop.
import React from "react";
class ParentComponent extends React.Component {
state = {
array: []
};
async getData() {
const { films: data } = await fetch(
"https://swapi.dev/api/planets/1/"
).then(res => res.json());
let content = [...data]; // object structure
console.log("--", content);
this.setState({
array: content
});
}
render() {
const { array } = this.state;
return (
<div>
<button
onClick={e => {
e.preventDefault();
this.getData();
}}
>
get data
</button>
{array.length > 0 && <div> {JSON.stringify(array)} </div>}
</div>
);
}
}
export default function App() {
return (
<div>
<ParentComponent />
</div>
);
}
State mutation methods inside render() will cause infinite re-rendering.
render() {
const { array } = this.state;
// uncomment me for infinity
//console.log(this.getData());
return (
<>
<button onClick={(e) => this.handleOnClick(e)}>getData</button>
<Child data={array} />
</>
);
}
codesandbox
I am checking to see if isFetchingData then don't render yet but its not re-rendering once isFetchingData is set to false. I have the useEffect in the context and i would hope that would re-render once isFetchingData is set to false. Any ideas?
When I refresh the page it renders with the data. So I think its to do with re-rendering.
I am using react context to get the data and exposing functions to filter that data and get me what i need.
Context:
import React, { useEffect, useState } from 'react';
import getAllEmployees from 'my-services/employee/getAllEmployees';
import { arrayOf, node, oneOfType } from 'prop-types';
export const EmployeeContext = React.createContext({
allEmployees: [],
getActiveEmployees: () => [],
getTerminatedEmployees: () => []
});
const EmployeesProvider = ({ children }) => {
const [isFetchingData, setIsFetchingData] = useState(true);
const [allEmployees, setAllEmployees] = useState({});
useEffect(() => {
getAllEmployees().then(
//doing something
).then(employees => {
setAllEmployees(employees);
setIsFetchingData(false);
});
}, [isFetchingData])
const context = {
isFetchingData,
allEmployees,
getActiveEmployees: () =>
allEmployees.filter(x => x.status === 'Current'),
getTerminatedEmployees: () =>
allEmployees.filter(x => x.status === 'Terminated')
};
return (
<EmployeeContext.Provider value={context}>{children}</EmployeeContext.Provider>
);
};
EmployeesProvider.propTypes = {
children: oneOfType([node, arrayOf(node)])
};
EmployeesProvider.defaultProps = {
children: undefined
};
export default EmployeesProvider;
Component:
import React, { useContext } from 'react';
import styled from 'styled-components';
import { EmployeeContext } from 'my-contexts/EmployeeContext';
import EmployeeCard from '../../../components/EmployeeCard';
const EmployeesTab = () => {
const {
getActiveEmployees,
getTerminatedEmployees,
isFetchingData
} = useContext(EmployeeContext);
let activeEmployees = [];
let terminatedEmployees = [];
if (!isFetchingData) {
activeEmployees = getActiveEmployees();
terminatedEmployees = getTerminatedEmployees();
}
if(isFetchingData) {
return <p>Loading</p>;
}
return (
<Outer>
<TopHeader>
<H3>Employees ({activeEmployees.length})</H3>
</TopHeader>
<Wrapper>
{activeEmployees.map(employee => {
return (
<EmployeeCard
id={employee.id}
guid={employee.guid}
firstName={employee.name.first}
lastName={employee.name.last}
jobTitle={employee.jobTitle}
/>
);
})}
</Wrapper>
<H3>Terminated employees({terminatedEmployees.length})</H3>
<Wrapper>
{terminatedEmployees.map(employee => {
return (
<EmployeeCard
id={employee.id}
guid={employee.guid}
firstName={employee.name.first}
lastName={employee.name.last}
jobTitle={employee.jobTitle}
/>
);
})}
</Wrapper>
</Outer>
);
};
export default EmployeesTab;
I think many problems may exist.
At first, please check whether whole component is closed by context Provider.
For example
<EmployeesProvider>
<EmployeesTab/>
<EmployeesProvider/>
Please check this problem.
I need to use a different component depending on a propType, as a first attempt I'm using an object to store the components I need the problem is that It only works for the first key, for example it only works for AvatarList.Item, when I try to load Avatar.List it just doesn't load.
const component = {
AvatarList: {
Item: async () => (await import('../List/Avatar')).Avatar,
List: async () => (await import('../List/Avatar')).List,
},
Simple: {
List: async () => (await import('../List/Simple')).List,
Item: async () => (await import('../List/Simple')).Simple,
},
};
// Here there is the component and the default I componentType is "AvatarList"
class Articles extends Component {
renderListItem() {
const { componentType, newsArticles } = this.props;
const Item = importComponent(component[componentType].Item);
return newsArticles.map(({
url,
id,
imageUrl,
title,
description,
}) => (
<Item
id={id}
url={url}
imageUrl={imageUrl}
title={title}
description={description}
/>
));
}
renderList() {
const { componentType } = this.props;
const List = importComponent(component[componentType].List);
return (
<List>
{this.renderListItem()}
</List>
);
}
render() {
return (
this.renderList()
);
}
}
// This is the HOC I use for the loading the components with async/await
import React, { Component } from 'preact-compat';
import Loader from '../components/Loader/Loader';
export default function importComponent(importFunction) {
return class ComponentImporter extends Component {
async componentWillMount() {
this.setState({ component: await importFunction() });
}
render() {
const ImportedComponent = this.state.component;
return (
<Loader loaded={Boolean(this.state.component)}>
<ImportedComponent {...this.props} />
</Loader>
);
}
};
}
Apparently I can't get the named exports if they're coming from the same file dynamically, so the solution for this was just importing those files and get the variables on the componentDidMount.