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.
Related
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 have a React app which utilizes the context hook. The app functions properly but I am having difficulty writing passing tests.
My context looks like
import React, { createContext } from 'react';
const DataContext = createContext();
export const DataProvider = (props) => {
const [personSuccessAlert, setPersonSuccessAlert] = React.useState(false);
return (
<DataContext.Provider
value={{
personSuccessAlert,
setPersonSuccessAlert,
}}>
{props.children}
</DataContext.Provider>
);
};
export const withContext = (Component) => (props) => (
<DataContext.Consumer>
{(globalState) => <Component {...globalState} {...props} />}
</DataContext.Consumer>
);
The app uses this context in a useEffect hook
import React, { useEffect } from 'react';
import { Alert } from '../../../components/alert';
const PersonRecord = ({
match: {
params: { id },
},
setPersonSuccessAlert,
personSuccessAlert,
}) => {
const closeAlert = () => {
setTimeout(() => {
setPersonSuccessAlert(false);
}, 3000);
};
useEffect(() => {
closeAlert();
}, [location]);
return (
<>
<Alert
open={personSuccessAlert}
/>
</>
);
};
export default withContext(PersonRecord);
This all works as expected. When I run my tests I know I need to import the DataProvider and wrap the component but I keep getting an error.
test('useeffect', async () => {
const history = createMemoryHistory();
history.push('/people');
const setPersonSuccessAlert = jest.fn();
const { getByTestId, getByText } = render(
<DataProvider value={{ setPersonSuccessAlert }}>
<MockedProvider mocks={mocksPerson} addTypename={false}>
<Router history={history}>
<PersonRecord match={{ params: { id: '123' } }} />
</Router>
</MockedProvider>
</DataProvider>,
);
const alert = getByTestId('styled-alert');
await act(async () => new Promise((resolve) => setTimeout(resolve, 4000)));
});
There are a few different errors I get depending on how I change things up but the most common is
[TypeError: setPersonSuccessAlert is not a function]
I think my context is setup slightly different than others which is why I am having trouble using other methods found on here.
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'm trying to render data from props in React functional component that look like this:
interface TagsComponentProps {
tags: Tag[];
}
const TagsComponent: FC<TagsComponentProps> = (props: TagsComponentProps) => (
<>
{props.tags.length === 0 &&
<LoadingStateComponent />
}
{props.tags.map(tag => {
{ tag.tagId }
{ tag.tagName }
})
}
</>
)
export default TagsComponent;
Within Next.js page that receiving data inside the getStaticProps method. It looks like that:
const IndexPage = ({ tags }: InferGetStaticPropsType<typeof getStaticProps>) => (
<>
<LayoutComponent>
<TagsComponent tags={tags} />
</LayoutComponent>
</>
)
export default IndexPage;
export const getStaticProps = async () => {
const res = await fetch(`${process.env.HOST}/api/tags/read`)
const data = await res.json()
// if (error) {
// return <ErrorComponent errorMessage={'Ошибка загрузки тегов'} />
// }
return {
props: {
tags: data.Items as Tag[]
}
}
}
But nothing is getting rendered at all although I'm receiving data. Probably I'm missing some concept of data fetching for SSR in Next.js.
I guess the issue is .map() is not returning anything in your code here:
{props.tags.map(tag => {
{ tag.tagId }
{ tag.tagName }
})
}
Instead you should try as the following:
{
props.tags.map(tag => (
<>
{ tag.tagId }
{ tag.tagName }
</>
))
}
Also you can do a null check before as props.tags && props.tags.map().
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} />
);
}
};
};
};