React setState doesn't re-render after fetching data - javascript

This code works if data already fetched.
But doesn't work if I refresh the page and doesn't rerender element.
I'm also using Next JS if it's worth mentioning.
class Books extends Component {
constructor(props) {
super(props);
this.state = {
book: []
}
this.renderBooks= this.renderBooks.bind(this);
}
renderBooks() {
let item;
let items = [];
return new Promise((resolve, reject) => {
this.props.ids.forEach(address => {
firebase.database().ref(`/books/${address}`)
.on('value', snap => {
item = snap.val();
});
items.push(item);
})
resolve(items);
});
}
async componentDidMount() {
try {
let res = [];
res = await this.renderBooks();
console.log(res);
this.setState({ book: res });
} catch (error) {
console.log(error);
this.setState(prevState => {
return { book: 'err' }
});
}
}
render() {
return (
<div>
{ <List grid={{ gutter: 16 }}
dataSource={ this.state.book }
renderItem={ item => (
<List.Item>
<Card title={ !!item && item.title }>
...Book content...
</Card>
</List.Item>
)} />
}
</div>
);
}
}
export default Books;
Is there anything to know about setState and fetching data that I missed here?
PS. Edited constructor to book: [].

You cannot initialize book with a promise. Instead you can have a solution like below.
Add a conditional rendering to you render method so it will know when to render book. Also you don't need to return new Promise in this case.
class Books extends Component {
constructor(props) {
super(props);
this.state = { books: null }
}
componentDidMount() {
this.renderBooks()
}
renderBooks() {
this.props.ids.forEach(address => {
firebase.database().ref(`/books/${address}`)
.on('value', snap => {
this.setState({books: [...(this.state.books || []), snap.val()] });
});
});
}
render() {
return (
this.state.books ?
<div>
{ <List grid={{ gutter: 16 }}
dataSource={ this.state.books }
renderItem={ item => (
<List.Item>
<Card title={ !!item && item.title }>
...Book content...
</Card>
</List.Item>
)} />
}
</div>
: 'Initializing'
);
}
}
export default Books;
Promises are basically async functions that are resolved when it's time.
So when you do
var item, items = []; // <---- Step 1
this.props.ids.forEach(address => {
firebase.database().ref(`/books/${address}`)
.on('value', snap => {
item = snap.val(); // <--- Step 3
});
});
items.push(item); // <----- Step 2
});
The steps are like this. So you were doing items.push(item) before item was assigned a new value which is snap.val(). And that makes item undefined.
I guess the second result you have is thanks to caching. If the internet connection is SOOOO FAST Step 3 might be earlier than Step 2, but that's a bad assumption. That's why the second time you get the result correct.
In this answer's case, instead of having an items array, the snap.val() is added to this.state.books. But this makes it a bit heavyweight. Because every time a query on('value') is called, the setState method will be triggered and the component will be rendered again. If there were 1000 ids the state would change 1000 times.
That's why instead of getting the data one by one I would suggest you to get all the data at once. Try to google something like 'retrieve multiple data from firebase javascript'. Unfortunately I don't know much about firebase so cannot help there.

You are initializing this.state.book with a promise. Try setting it to null instead:
this.state = {
book: null
}

Related

How to set Ternary Operator "Loading" Div inside React Component with Two Return statements?

The API that I am using is slow. My React component is using FetchAPI and has two return statements.
I want to incorporate a ternary operator condition that will display a "Loading.." div while I wait for the data like this: https://codesandbox.io/s/api-fetch-using-usestate-useeffect-k024k
How do I include a "Loading" ternary with two return statements in my code below? Note: I drastically reduced my code to not drive anyone insane so I only included the overall structure. You're welcome!
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
fetch('https.data.json')
.then(response => response.json())
.then(data => this.setState({ data: data}))
}
render() {
let theList = this.state.map((item, id) => {
return (
<Card>A bunch of API Stuff </Card>
);
})
return (
<div className="search-wrap">
<Row>
{theList}
</Row>
</div>
)
}
}
export default App;
My React component is using FetchAPI and has two return statements.
No, it doesn't. It has one:
render() {
let theList = this.state.map((item, id) => {
return (
<Card>A bunch of API Stuff </Card>
);
})
return ( // <========== here
<div className="search-wrap">
<Row>
{theList}
</Row>
</div>
)
}
The other one is a return within your map callback, which doesn't have any effect on your render method at all.
If you want to use the conditional operator to show a loading indicator, you'd do it in your render's return:
render() {
let { loading } = this.state; // *** Get the flag
let theList = this.state.map((item, id) => {
return (
<Card>A bunch of API Stuff </Card>
);
})
return ( // *** Use it in the below
<div className="search-wrap">
<Row>
{loading ? <p>Loading...</p> : theList}
</Row>
</div>
)
}
You might also want to avoid the unnecessary map call, but if you know your state is initialized with an empty array, that call is harmless. But if you want to get rid of it:
render() {
let { loading } = this.state; // *** Get the flag
let theList = !loading && this.state.map((item, id) => {
return (
<Card>A bunch of API Stuff </Card>
);
})
return ( // *** Use it in the below
<div className="search-wrap">
<Row>
{loading ? <p>Loading...</p> : theList}
</Row>
</div>
)
}
Just add a boolean field to your state which indicates that data is being loaded.
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true, // this one
};
}
componentDidMount() {
fetch('https.data.json')
.then(response => response.json())
.then(data => this.setState({ data: data, loading: false }))
}
render() {
if (this.state.loading)
return <div>Loading...</div>
let theList = this.state.map((item, id) => {
return (
<Card>A bunch of API Stuff </Card>
);
})
return (
<div className="search-wrap">
<Row>
{theList}
</Row>
</div>
)
}
}
export default App;
If I understand correctly, your code should look like the following:
class App extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
isLoading: false
};
}
componentDidMount() {
this.setState({ isLoading: true }); // mount the loading component when we will start the fecth
fetch('https.data.json')
.then(response => response.json())
.then(data => this.setState({ data: data}))
.then(() => this.setState({ isLoading: false })) // unmount Loading component when the fetch ends
}
render() {
let theList = this.state.map((item, id) => {
return (
<Card>A bunch of API Stuff </Card>
);
})
// Here we will return the Loading component as long as isLoading is true
return (isLoading) ? <Loading /> : (
<div className="search-wrap">
<Row>
{theList}
</Row>
</div>
)
}
}
export default App;
So basically we will add a boolean variable (isLoading) that will handle the state of the fetch and we will add it into the state of the component. When the fetch is triggered, this variable will be true otherwise it will be false.
So then in the return statement, we can use a ternary operator based on the value of this new variable. If it is true we will return a Loading component or a simple div saying Loading.... Otherwise we will return the App component with the data loaded.
I hope this help :)
You can do something like this. Note that you can export directly the class since the beginning. Also, you can simplify the state, without a explicit constructor.
To know the fetch state, you should add a isLoading condition before and after fetching the data. Then, in the render, you can return one single node, and inside render the components that you want based on your status. With this coding style, you can even show when the fetch returns an empty array.
export default class App extends Component {
state = {
data: [],
isLoading: false
}
componentDidMount() {
this.setState({
isLoading: true
}, () => {
fetch('https.data.json')
.then(response => response.json())
.then(data => this.setState({
data,false
}))
})
}
render() {
return (
<div>
{
this.state.isLoading &&
<span>
Loading...
</span>
}
{
!this.state.isLoading &&
(this.state.data.length > 0) &&
<div className="search-wrap">
<Row>
{
this.state.data.map((item, id) => {
return (
<Card key={id}>
A bunch of API Stuff
</Card>
);
})
}
</Row>
</div>
}
{
!this.state.isLoading &&
(this.state.data.length === 0) &&
<span>
There's no data to show
</span>
}
</div>
)
}
}
Hope this helps!
Denny

Filtering an Array within an Array in React

import React, { Component } from "react"
import {
StaticQuery,
grahpql,
Link
} from "gatsby"
import {
StyledFilter,
StyledLine
} from "./styled"
class Filter extends Component {
render() {
const { data } = this.props
const categories = data.allPrismicProjectCategory.edges.map((cat, index) => {
return (
<a
key={index}
onClick={() => this.props.setFilterValue(cat.node.uid)}
>
{cat.node.data.category.text}
</a>
)
})
return (
<StyledFilter>
<div>
Filter by<StyledLine />
<a
// onClick={() => {this.props.filterProjects("all")}}
>
All
</a>
{categories}
</div>
<a onClick={this.props.changeGridStyle}>{this.props.gridStyleText}</a>
</StyledFilter>
)
}
}
export default props => (
<StaticQuery
query={graphql`
query {
allPrismicProjectCategory {
edges {
node {
uid
data {
category {
text
}
}
}
}
}
}
`}
render={data => <Filter data={data} {...props} />}
/>
)
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
I am working on a React App with Gatsby and Prismic that has a project page. By default it lists all projects but at the page's top appears a filter to select by category (just a bunch of <a> tags).
My Page consists of a <Filter /> component as well as several <GridItem /> components I am mapping over and load some props from the CMS.
The part I am struggling with is the filtering by category.
When my page component mounts it adds all projects into my filteredItems state.
When a user is clicking on a filter at the top it set's my default filterValue state from "all" to the according value.
After that I'll first need to map over the array of projects and within that array I'll need to map over the categories (each project can belong to multiple categories).
My idea is basically if a value (the uid) matches my new this.state.filterValue it returns the object and add's it to my filteredItems state (and of course delete the one's not matching this criteria).
This is what my page component looks like (cleaned up for better readability, full code in the snippet at the bottom):
class WorkPage extends Component {
constructor(props) {
super(props)
this.state = {
filterValue: "all",
filteredItems: []
}
this.filterProjects = this.filterProjects.bind(this)
}
filterProjects = (filterValue) => {
this.setState({ filterValue: filterValue }, () =>
console.log(this.state.filterValue)
)
// see a few of my approaches below
}
componentDidMount() {
this.setState({
filteredItems: this.props.data.prismicWork.data.projects
})
}
render() {
const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
const item = node.project_item.document["0"].data
const categories = node.project_item.document["0"].data.categories.map(cat => {
return cat.category_tag.document["0"].uid
})
return (
<GridItem
key={index}
categories={categories}
moreContentProps={moreContentProps}
/>
)
})
return (
<LayoutDefault>
<Filter
filterProjects={this.filterProjects}
/>
{projectItems}
</LayoutDefault>
)
}
}
I tried so many things, I can't list all of them, but here are some examples:
This approach always returns an array of 10 objects (I have 10 projects), sometimes the one's that don't match the this.state.filterValue are empty objects, sometimes they still return their whole data.
let result = this.state.filteredItems.map(item => {
return item.project_item.document["0"].data.categories.filter(cat => cat.category_tag.document["0"].uid === this.state.filterValue)
})
console.log(result)
After that I tried to filter directly on the parent item (if that makes sense) and make use of indexOf, but this always console logged an empty array...
let result = this.state.filteredItems.filter(item => {
return (item.project_item.document["0"].data.categories.indexOf(this.state.filterValue) >= 0)
})
console.log(result)
Another approach was this (naive) way to map over first the projects and then the categories to find a matching value. This returns an array of undefined objects.
let result = this.state.filteredItems.map(item => {
item = item.project_item.document["0"].data.categories.map(attachedCat => {
if (attachedCat.category_tag.document["0"].uid === this.state.filterValue) {
console.log(item)
}
})
})
console.log(result)
Other than that I am not even sure if my approach (having a filteredItems state that updates based on if a filter matches the according category) is a good or "right" React way.
Pretty stuck to be honest, any hints or help really appreciated.
import React, { Component } from "react"
import { graphql } from "gatsby"
import LayoutDefault from "../layouts/default"
import { ThemeProvider } from "styled-components"
import Hero from "../components/hero/index"
import GridWork from "../components/grid-work/index"
import GridItem from "../components/grid-item/index"
import Filter from "../components/filter/index"
class WorkPage extends Component {
constructor(props) {
super(props)
this.state = {
filterValue: "all",
filteredItems: [],
isOnWorkPage: true,
showAsEqualGrid: false
}
this.filterProjects = this.filterProjects.bind(this)
this.changeGridStyle = this.changeGridStyle.bind(this)
}
changeGridStyle = (showAsEqualGrid) => {
this.setState(prevState => ({
showAsEqualGrid: !prevState.showAsEqualGrid,
isOnWorkPage: !prevState.isOnWorkPage
}))
}
filterProjects = (filterValue) => {
this.setState({ filterValue: filterValue }, () =>
console.log(this.state.filterValue)
)
let result = this.state.filteredItems.filter(item => {
return (item.project_item.document["0"].data.categories.toString().indexOf(this.state.filterValue) >= 0)
})
console.log(result)
}
componentDidMount() {
this.setState({
filteredItems: this.props.data.prismicWork.data.projects
})
}
render() {
const projectItems = this.props.data.prismicWork.data.projects && this.props.data.prismicWork.data.projects.map((node, index) => {
const item = node.project_item.document["0"].data
const categories = node.project_item.document["0"].data.categories.map(cat => {
return cat.category_tag.document["0"].uid
})
return (
<GridItem
key={index}
isSelected="false"
isOnWorkPage={this.state.isOnWorkPage}
isEqualGrid={this.state.showAsEqualGrid}
projectURL={`/work/${node.project_item.uid}`}
client={item.client.text}
tagline={item.teaser_tagline.text}
categories={categories}
imageURL={item.teaser_image.squarelarge.url}
imageAlt={item.teaser_image.alt}
/>
)
})
return (
<ThemeProvider theme={{ mode: "light" }}>
<LayoutDefault>
<Hero
introline="Projects"
headline="Art direction results in strong brand narratives and compelling content."
/>
{/* {filteredResult} */}
<Filter
filterProjects={this.filterProjects}
changeGridStyle={this.changeGridStyle}
gridStyleText={this.state.showAsEqualGrid ? "Show Flow" : "Show Grid"}
/>
<GridWork>
{projectItems}
</GridWork>
</LayoutDefault>
</ThemeProvider>
)
}
}
export default WorkPage
export const workQuery = graphql`
query Work {
prismicWork {
data {
page_title {
text
}
# All linked projects
projects {
project_item {
uid
# Linked Content
document {
type
data {
client {
text
}
teaser_tagline {
text
}
teaser_image {
url
alt
xlarge {
url
}
large {
url
}
medium {
url
}
squarelarge {
url
}
squaremedium {
url
}
squaresmall {
url
}
}
categories {
category_tag {
document {
uid
data {
category {
text
}
}
}
}
}
}
}
}
}
}
}
}
`
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.6.3/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.6.3/umd/react-dom.production.min.js"></script>
So there are at least two things.
In your filterProjects() you're first setting state.filterValue and then you use it in filteredItems.filter(). That might not work, because React does not execute setState() immediately always, to optimize performance. So you're probably filtering against the previous value of state.filterValue. Instead just use filterValue, which you pass into filterProjects().
setFilterValue = (filterValue) => {
this.setState({filterValue}) // if key and variable are named identically, you can just pass it into setState like that
}
// arrow function without curly braces returns without return statement
filterProjects = (projects, filterValue) =>
projects.filter(item => item.project_item.document[0].data.categories.toString().includes(filterValue))
You should return the result from filterProjects(), because you need to render based on the filteredItems then, of course. But actually it's not necessary to put the filter result into state. You can apply the filterProjects() on the props directly, right within the render(). That's why you should return them. Also separate setState into another function which you can pass into your <Filter/> component.
And a recommendation: Use destructuring to make your code more readable. For you and anyone else working with it.
render() {
const { projects } = this.props.data.prismicWork.data // this is
const { filterValue } = this.state // destructuring
if (projects != undefined) {
this.filterProjects(projects, filterValue).map((node, index) => {
// ...
// Filter component
<Filter filterProjects={this.setFilterValue} />
That way you trigger a rerender by setting the filterValue, because it
resides in this.state, and the render function depends on
this.state.filterValue.
Please try that out and tell me if there is another problem.

React - is it bad practice to pass up a callback?

I have a REACT app which is basically a till for adding items to an order. I have my OrderSection which does most of the grunt work, including having a barcode scanner, and I have my Search component which is a child of OrderSection, and if someone clicks on a search result it passes that back up to OrderSection via a prop callback.
Now, this is what I initially had, but it had problems:
#autobind
class OrderSection extends React.Component {
constructor(props) {
super(props);
this.state = {
orderItems: [],
multiMatch: [],
};
}
async barcodeScanner(barcode) {
let response;
try {
response = await serverApi.getItemsFromBarcode(barcode);
} catch(e) {
return toast.error(e.message || e.toString());
}
let {items} = response;
if (items.length === 0) {
toast.info('no matching items found');
} else if (items.length === 1) {
this.addItem(items[0]);
} else {
// show results in the 'search' section
this.setState({multiMatch: items})
}
}
addItem(item) {
// doesn't really matter what happens here
}
async lookupAdd(no, code) {
try {
let {items} = await serverApi.getItems(no, code);
let item = items[0];
if (item) {
this.addItem(item);
} else {
}
} catch(e) {
toast.error(e.toString());
}
}
render() {
return (
<section>
// render items up here
<Search
onItemClick={this.lookupAdd}
results={this.state.multiMatch} />
</section>
)
}
}
#autobind
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
show: false // change to true to show the search
}
}
// code for updating search results on form submit
// updating this.state.searchResults
render() {
return (
<React.Fragment>
// form with search text input here
// render searchResults here
</React.Fragment>
)
}
componentWillReceiveProps(props) {
if (props.results.length) {
this.setState({searchResults: props.results, show: true});
}
}
}
Search.propTypes = {
onItemClick: PropTypes.func.isRequired,
results: PropTypes.array
};
The main issue here is how in OrderSection, in barcodeScanner, when I have multiple matches, I pass them down as a prop into Search, and then Search sees that prop and updates itself in the componentWillReceiveProps function.
I wasn't entirely happy with what was happening there -- it was actually fine most of the time, but there was some annoying unexpected behaviour of Search showing itself when the prop actually hadn't changed.
So I came up with the idea of passing a callback up from Search to OrderSection:
#autobind
class OrderSection extends React.Component {
constructor(props) {
super(props);
this.state = {
orderItems: []
};
}
async barcodeScanner(barcode) {
let response;
try {
response = await serverApi.getItemsFromBarcode(barcode);
} catch(e) {
return toast.error(e.message || e.toString());
}
let {items} = response;
if (items.length === 0) {
toast.info('no matching items found');
} else if (items.length === 1) {
this.addItem(items[0]);
} else {
// show results in the 'search' section
this.sendMultiMatchToSearch(items);
}
}
setSearchResultsFunc(func) {
this.sendMultiMatchToSearch = func;
}
addItem(item) {
// doesn't really matter what happens here
}
async lookupAdd(no, code) {
try {
let {items} = await serverApi.getItems(no, code);
let item = items[0];
if (item) {
this.addItem(item);
} else {
}
} catch(e) {
toast.error(e.toString());
}
}
render() {
return (
<section>
// render items up here
<Search
onItemClick={this.lookupAdd}
manuallySetResultsFunc={this.setSearchResultsFunc}
/>
</section>
)
}
}
#autobind
class Search extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults: [],
show: false // change to true to show the search
};
if (typeof this.props.manuallySetResultsFunc === "function") {
const func = (results) => {
this.setState({searchResults: results, show: true});
this.flash();
};
this.props.manuallySetResultsFunc(func);
}
}
render() {
return (
<React.Fragment>
// render searchResults here
</React.Fragment>
)
}
}
Search.propTypes = {
onItemClick: PropTypes.func.isRequired,
manuallySetResultsFunc: PropTypes.func
};
But I feel like this is probably bad react practice. It's producing the behavior I want but I think if a React expert looks at this they wouldn't like it.
Can I get some advice on the proper way to pass search results down to Search to trigger it, while still otherwise allowing the SEARCH element to control its own searchResults code
You're right in that you shouldn't have to 'intervene' in this way to modify how your state is updated. You should just set up your state and props and then things should take care of themselves.
Here are some straightforward approaches that I'd typically use:
1) From the OrderSection parent to conditionally render your Search only when there are items:
render() {
return (
<section>
{this.state.multiMatch && <Search
onItemClick={this.lookupAdd}
manuallySetResultsFunc={this.setSearchResultsFunc}
/>}
</section>
)
}
2) Within the <Search> child:
render() {
return (
<React.Fragment>
{this.state.searchResults && this.state.searchResults.map(result=> // map to SearchResults)}
</React.Fragment>
)
}
3) From the OrderSection parent pass in 'isShowing' as a prop:
render() {
const isShowing = !!this.state.multiMatch; // add other logic here if necessary
return (
<section>
<Search
onItemClick={this.lookupAdd}
isShowing={isShowing}
/>
</section>
)
}
Then in your Search, extract isShowing from props.
The idea is that you only need to update the state and the rendering should take care of itself.
I would introduce additional props to Search component showMultiMatch and onSearchClose and add showSearch to OrderSection component(which is set to true when you receive multiMatch and set to false in the onSearchClose handler). Remove componentWillReceiveProps and check condition this.props.showMultiMatch || this.state.show in the render function to render search conditionally.

How to search data efficiently with React Native ListView

I am trying to implement a filter and search function that would allow user to type in keyword and return result(array) and re-render the row
This is the event arrays that being passed in into the createDataSource function
The problem I am having now is my search function can't perform filter and will return the entire parent object although I specifically return the indexed object.
Here's what I got so far
class Search extends Component {
state = { isRefreshing: false, searchText: '' }
componentWillMount() {
this.createDataSource(this.props);
}
componentWillReceiveProps(nextProps) {
this.createDataSource(nextProps);
if (nextProps) {
this.setState({ isRefreshing: false })
}
}
createDataSource({ events }) {
const ds = new ListView.DataSource({
rowHasChanged: (r1, r2) => r1 !== r2
});
this.dataSource = ds.cloneWithRows(events);
}
//return arrays of event from events
renderRow(event) {
return <EventItem event={event} />;
}
onRefresh = () => {
this.setState({ isRefreshing: true });
this.props.pullEventData()
}
setSearchText(event) {
let searchText = event.nativeEvent.text;
this.setState({ searchText })
var eventLength = this.props.events.length
var events = this.props.events
const filteredEvents = this.props.events.filter(search)
console.log(filteredEvents);
function search() {
for (var i = 0; i < eventLength; i++) {
if (events[i].title === searchText) {
console.log(events[i].title)
return events[i];
}
}
}
}
render() {
const { skeleton, centerEverything, container, listViewContainer, makeItTop,
textContainer, titleContainer, descContainer, title, desc, listContainer } = styles;
return(
<View style={[container, centerEverything]}>
<TextInput
style={styles.searchBar}
value={this.state.searchText}
onChange={this.setSearchText.bind(this)}
placeholder="Search" />
<ListView
contentContainerStyle={styles.listViewContainer}
enableEmptySections
dataSource={this.dataSource}
renderRow={this.renderRow}
refreshControl={
<RefreshControl
refreshing={this.state.isRefreshing}
onRefresh={this.onRefresh}
title="Loading data..."
progressBackgroundColor="#ffff00"
/>
}
/>
</View>
)
}
}
As you can see from the image above, my code requires me to type in the full query text to display the result. And it displays all the seven array objects? why's that?
The syntax of Array.prototype.filter is wrong... it should take a callback that will be the item being evaluated for filtering.. if you return true it will keep it.
function search(event) {
return ~event.title.indexOf(searchText)
}
You could even make the inline like..
const filteredEvents = this.props.events.filter(event => ~event.title.indexOf(searchText))
For understanding my use of ~, read The Great Mystery of the Tilde.
Since filter returns a new array, you should be able to clone your dataSource with it. If you didn't use filter, you would have to call events.slice() to return a new array. Otherwise, the ListView doesn't pickup the changes.

How can I prevent a component from rendering before data is loaded?

I am waiting the props to come up from a store named GetDealersStore, and the way I am fetching that data is with an action where I am doing this:
componentWillMount () { GetDealersActions.getDealers(); }
I already test the app and componentWillMount() is running before the initial render where I have this
let dealerInfo;
if (this.state.dealerData) {
dealerInfo = this.state.dealerData.dealersData.map((dealer) => {
return (<div>CONTENT</div>);
})
} else {
dealerInfo = <p>Loading . . .</p>
}
but for the first second you can see <p>Loading . . .</p> in the screen which is the else in the conditional above, and then the rest of the render comes up with return (<div>CONTENT</div>); which is the if in the conditional. So, I guess, this means that the render method has been trigger twice because it keeps waiting for the data coming from the database.
The data from the database is not available at the time of the 1st render, so, how can I fetch that data before the 1st initial render occurs?
You can't do this with a single component. You should follow the Container Component pattern to separate data from rendering.
let DealersContainer = React.createClass({
getInitialState() {
return {dealersData: []};
},
componentWillMount() {
GetDealersActions.getDealers();
},
render() {
let {dealersData} = this.state;
return (<div>
{dealersData.map((dealer) => {
let props = dealer;
return (<Dealer ...props />); // pass in dealerData as PROPS here
})}
</div>);
}
});
Then update your Dealer component to receive props and render the actual content.
My answer is similar to Mathletics', just in more detail.
In this example I've included initialising state of dealerData to null; this is the check that's made to determine whether the data has been returned from the store by the container.
It's verbose, but declarative, and does what you want, in the order that you want, and it will work each time.
const DealerStore = MyDataPersistenceLibrary.createStore({
getInitialState() {
return {
dealerData: null
};
},
getDealers() {
// some action that sets the dealerData to an array
}
});
const DealerInfoContainer = React.createClass({
componentWillMount() {
DealerStoreActions.getDealers();
},
_renderDealerInfo() {
return (
<DealerInfo {...this.state} />
);
},
_renderLoader() {
return (
<p>Loading...</p>
);
},
render() {
const { dealerData } = this.state;
return (
dealerData
? this._renderDealerInfo()
: this._renderLoader()
);
}
});
const DealerInfo = React.createClass({
getDefaultProps() {
return {
dealerData: []
};
},
_renderDealers() {
const { dealerData } = this.props;
return dealerData.map(({ name }, index) => <div key={index}>{name}</div>);
},
_renderNoneFound() {
return (
<p>No results to show!</p>
);
},
render() {
const { dealerData } = this.props;
return (
dealerData.length
? this._renderDealers()
: this._renderNoneFound()
);
}
});

Categories

Resources