React: following DRY in React when fetching data from API - javascript

When I am fetching the data from API, If I get Ok I will render the component to display the data,
If an error has occured, I want to display a custom message for different components, and during fetching the data I want to show a loading message.
export default function App() {
const { data: products, loading, error } = useFetch(
"products?category=shoes"
);
if (error) return <div>Failed to load</div>;
if (loading) return <div>wait is Loading</div>;
return (
<>
<div className="content">
<section id="products">{products.map((product) =>{return <div>product.name</div>})}</section>
</div>
</>
);
}
I am planning to have a fetch call in multiple components. So, I was thinking If I could write the above logic in one place, like an HOC or Rendered Props and reuse it.
What I tried: but failed
HOC wont work because you cant call hooks in normal functions. It has to be a Component.
Rendered Props wont work as it gives this error:
Error: Rendered more hooks than during the previous render.
Below is my failed rendered props code
const ShowData= function ({ data: products }) {
return (
<div>
{products.map(product => {return <div>product.name</div>})}
</div>
);
};
function SearchCount({ count }) {
return <h3>Search Results: {count} items found</h3>;
}
const Wrapper = function () {
return (
<LoadingAndError
render={ShowData}
params={{ url: "products?category=shoes" }}
></LoadingAndError>
);
};
export default Wrapper;
Loading and error logic is moved to the LoadingAndError component
const LoadingAndError = function (props) {
const { url } = props.params;
const { data: products, loading,error} = useFetch(url);
if (loading)
return <h1>Loading</h1>;
if (error) return <h1>Error</h1>;
return props.render({ data: products });
};

return props.render({ data: products });
This line is calling props.render as a function, but you've passed in a component. Calling component directly can cause exactly the error you're seeing. If you want props.render to be used like this, then you should pass in a function which creates an element, like this:
<LoadingAndError
render={(props) => <ShowData {...props} />}
params={{ url: "products?category=shoes" }}
></LoadingAndError>
Alternatively, if you want to keep passing in render={ShowData}, then change loadingAndError to create an element:
const loadingAndError = function (props) {
const { url } = props.params;
const { data: products, loading,error} = useFetch(url);
if (loading)
return <h1>Loading</h1>;
if (error) return <h1>Error</h1>;
const Component = props.render
return <Component data={products}/>
}

Related

Passing Async State to Next.js Component via Prop

I'm fetching WordPress posts asynchronously via getStaticProps()...
export async function getStaticProps({ params, preview = false, previewData }) {
const data = await getPostsByCategory(params.slug, preview, previewData)
return {
props: {
preview,
posts: data?.posts
},
}
}
... and passing them to useState:
const [filteredArticles, setFilteredArticles] = useState(posts?.edges)
Then, I pass the state to a component:
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<ArticleGrid myArticles={filteredArticles} />
This is necessary because another component will setFilteredArticles with a filter function.
But when we are passing the state to ArticlesGrid, the data is not ready when the component loads. This is confusing to me since we passing the state within a router.isFallback condition.
Even if we set state within useEffect...
const [filteredArticles, setFilteredArticles] = useState()
useEffect(() => {
setFilteredArticles(posts)
}, [posts?.edges])
... the data arrives too late for the component.
I'm new to Next.js. I can probably hack my way through this, but I assume there's an elegant solution.
Let's look at some useEffect examples:
useEffect(() => {
console.log("Hello there");
});
This useEffect is executed after the first render and on each subsequent rerender.
useEffect(() => {
console.log("Hello there once");
}, []);
This useEffect is executed only once, after the first render.
useEffect(() => {
console.log("Hello there when id changes");
}, [props.id]);
This useEffect is executed after the first render, every time props.id changes.
Some possible solutions to your problem:
No articles case
You probably want to treat this case in your ArticleGrid component anyway, in order to prevent any potential errors.
In ArticleGrid.js:
const {myArticles} = props;
if(!myArticles) {
return (<div>Your custom no data component... </div>);
}
// normal logic
return ...
Alternatively, you could also treat this case in the parent component:
{
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<>
{
filteredArticles
? <ArticleGrid myArticles={filteredArticles} />
: <div>Your custom no data component... </div>
}
</>
)
}
Use initial props
Send the initial props in case the filteres haven't been set:
const myArticles = filteredArticles || posts?.edges;
and then:
<ArticleGrid myArticles={myArticles} />

Not able to pass data from a page using getStaticProps to a child component

I'm working on a next.js project and I'm trying to pass data from "gymlist" page to "table" component.
this is the "gymlist" page where I have the getStaticProps function:
export default function Gyms({gyms}) {
return (
<Table gyms={gyms}/>
)
}
export async function getStaticProps() {
const res = await fetch(`${server}/api/gyms`)
const gyms = await res.json()
console.log(gyms);
if (!gyms) {
return {
notFound: true,
}
}
return {
props: {
gyms,
// Will be passed to the page component as props
},
};
}
the console.log shows the list of the gyms that I have in the database correctly.
This is the table component where I get the error: gyms.map is not a function
export default function Table(props) {
const [gyms, setGyms] = useState(props);
return (
{gyms?.map((gym, index) => (
<p> {gym.name} </p>
))}
);
}
As you try to use props.gyms to your useState default value, you are passing an object (the props object: { gyms: [...] }), instead of the gyms array.
It could be solved by passing props.gyms to useState: useState(props.gyms), or simply using props.gyms directly in your component render: {props.gyms.map(...)}.
You don't need state, in this case.

data fetched from api is not passed as props to nested component

In my react app I have a main App component where I fetched data from api in componentDidMount method and saved it in its state. Then I passed that state to another component in App. However, when I consume that data from prop it is showing undefined.
Another strange this I didn't is when I console.log state in App components render method them first I get an empty array then after a second another array with the data in it. Please help me here
The code goes like this-
class App extends React.Component {
constructor() {
super();
this.state = {
data: []
};
}
componentDidMount() {
this.fetchData();
}
fetchData() {
fetch(
`https://api.themoviedb.org/3/movie/popular?api_key=${
process.env.REACT_APP_API_KEY
}&language=en-US&page=1`
)
.then(res => res.json())
.then(data => {
this.setState({ data });
})
.catch(err => console.log(err));
}
render() {
console.log(this.state.data);
return (
<div>
<Movie title="Popular" results={this.state.data} />
</div>
);
}
}
export default App;
this.state.data is undefined in Movie component which is like this
function Movie(props) {
return (
<p>{props.data.results[0].title}</p>
)
}
You are passing results and title as the props to the <Movie> component yet trying to fetch data prop.
component:
<Movie title="Popular" results={this.state.data} />
So you need to fetch the results prop, not the data one.
fixed:
function Movie(props) {
return (
<p>{props.results[0].title}</p>
)
}
additionaly:
If you're already passing the title prop, why not just use that prop for the title?
function Movie(props) {
return (
<p>{props.title}</p>
)
}
Your prop is results so you need to reference that in your component:
function Movie(props) {
return (
<p>{props.results[0].title}</p>
)
}
In your App component you might also want to add a check in render to show a loading message or spinner if the data load hasn't been resolved when the component initially renders:
if (!this.state.data.length) return <Spinner />;

How to automatically run a function after a page loads in react?

In my componentDidMount(), I am calling an actionCreator in my redux file to do an API call to get a list of items. This list of items is then added into the redux store which I can access from my component via mapStateToProps.
const mapStateToProps = state => {
return {
list: state.list
};
};
So in my render(), I have:
render() {
const { list } = this.props;
}
Now, when the page loads, I need to run a function that needs to map over this list.
Let's say I have this method:
someFunction(list) {
// A function that makes use of list
}
But where do I call it? I must call it when the list is already available to me as my function will give me an error the list is undefined (if it's not yet available).
I also cannot invoke it in render (before the return statement) as it gives me an error that render() must be pure.
Is there another lifecycle method that I can use?
Just do this, and in redux store please make sure that initial state of list should be []
const mapStateToProps = state => {
return {
list: someFunction(state.list)
};
};
These are two ways you can play with received props from Redux
Do it in render
render() {
const { list } = this.props;
const items = list && list.map((item, index) => {
return <li key={item.id}>{item.value}</li>
});
return(
<div>
{items}
</div>
);
}
Or Do it in componentWillReceiveProps method if you are not using react 16.3 or greater
this.state = {
items: []
}
componentWillReceiveProps(nextProps){
if(nextProps.list != this.props.list){
const items = nextProps.list && nextProps.list.map((item, index) => {
return <li key={item.id}>{item.value}</li>
});
this.setState({items: items});
}
}
render() {
const {items} = this.state;
return(
<div>
{items}
</div>
);
}
You can also do it in componentDidMount if your Api call is placed in componentWillMount or receiving props from parent.

How store data from fetch

I'm pretty new in React and need some help.
I wanted display data from a movie database based on the search term. I'm using fetch inside my getMovies methode to get the data. The data is stored in data.Search but I don't know how to access it and store it in a variable.
class DataService
{
getMovies (searchTerm) {
//let dataStorage; //store somehow data.Search inside
fetch("http://www.omdbapi.com/?s=" + searchTerm, {
method: 'get'
})
.then((resp) => resp.json())
.then(function(data) {
return data.Search;
}).catch(function(error) {
console.log(error);// Error :(
});
}
//return dataStorage; //return data.Search
}
The below code is the correct react's way for your case, as simple as this:
import React from 'react';
export default class DataService extends React.Component {
constructor(props) {
super(props);
this.state = {
search_data: [], //for storing videos
};
this.getMovies = this.getMovies.bind(this); //bind this function to the current React Component
}
getMovies (searchTerm) {
fetch("http://www.omdbapi.com/?s=" + searchTerm, {
method: 'get'
})
.then((resp) => resp.json())
.then((data) => { //so that this callback function is bound to this React Component by itself
// Set state to bind search results into "search_data"
// or you can set "dataStorage = data.Search" here
// however, fetch is asynchronous, so using state is the correct way, it will update your view automatically if you render like I do below (in the render method)
this.setState({
search_data: data.Search,
});
});
}
componentDidMount() {
this.getMovies(); //start the fetch function here after elements appeared on page already
}
render() {
return (
{this.state.search_data.map((video, i) =>{
console.log(video);
// please use the correct key for your video below, I just assume it has a title
return(
<div>{video.title}</div>
)
})}
);
}
}
Feel free to post here any errors you may have, thanks
There are several ways of doing asynchronous tasks with React. The most basic one is to use setState and launch a Promise in the event handler. This might be viable for basic tasks but later on, you will encounter race conditions and other nasty stuff.
In order to do so, your service should return a Promise of results. On the React side when the query changes, the service is called to fetch new results. While doing so, there are few state transitions: setting loading flag in order to notify the user that the task is pending and when the promise resolves or rejects the data or an error is stored in the component. All you need is setState method.
More advanced techniques are based on Redux with redux-thunk or redux-saga middlewares. You may also consider RxJS - it is created especially for that kind of stuff providing debouncing, cancellation and other features out of the box.
Please see the following example of simple search view using yours DataService.
class DataService
{
//returns a Promise of search results, let the consumer handle errors
getMovies (searchTerm) {
return fetch("http://www.omdbapi.com/?s=" + searchTerm, {
method: 'get'
})
.then((resp) => resp.json())
.then(function(data) {
return data.Search;
})
}
}
const SearchResults = ({data, loading, error}) =>
<div>
{
data && data.map(({Title, Year, imdbID, Type, Poster}) =>
<div key={imdbID}>
{Title} - {Year} - {Type}
<img src={Poster} />
</div>
)
}
{loading && <div>Loading...</div>}
{error && <div>Error {error.message}</div>}
</div>
class SearchExample extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this)
this.state = {
data: [],
loading: false
};
}
handleChange(event) {
const service = new DataService();
//search query is target's value
const promise = service.getMovies(event.target.value);
//show that search is being performed
this.setState({
loading: true
})
//after the promise is resolved display the data or the error
promise.then(results => {
this.setState({
loading: false,
data: results
})
})
.catch(error => {
this.setState({
loading: false,
error: error
})
})
}
render() {
return (
<div>
<input placeholder="Search..." onChange={this.handleChange} type="search" />
<SearchResults data={this.state.data} loading={this.state.loading} error={this.state.error} />
</div>
)
}
}
ReactDOM.render(<SearchExample />, document.getElementById("app"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

Categories

Resources