React render() is being called before componentDidMount() - javascript

In my componentDidMount() I am making an API call to fetch some data, this call then sets a state object that I use in my render.
componentDidMount() {
const { actions } = this.props;
this.increase = this.increase.bind(this);
// api call from the saga
actions.surveyAnswersRequest();
// set breadcrumb
actions.setBreadcrumb([{ title: 'Score' }]);
actions.setTitle('Score');
this.increase();
}
In my render function I pass some prop values onto the view file:
render() {
const { global, gallery, survey_answers, survey, survey_actual_answers } = this.props;
if (global.isFetching) {
return <Loading />;
}
return this.view({ gallery, survey_answers, survey, survey_actual_answers });
}
The problem I am having is that the survey_actual_answers prop is not being set the first time that the page is loaded, however when I refresh the page the prop returns the data fine and the rest of the script will run. It's only the first time that it returns an empty array for that prop value.
This is how I have passed my props in:
Score.propTypes = {
actions: PropTypes.object.isRequired,
global: PropTypes.object.isRequired,
survey: PropTypes.object.isRequired,
survey_answers: PropTypes.object.isRequired,
gallery: PropTypes.object.isRequired,
survey_actual_answers: PropTypes.array.isRequired,
survey_score_system: PropTypes.array.isRequired,
survey_styles: PropTypes.object.isRequired,
survey_general_doc_data: PropTypes.object.isRequired
};
function mapStateToProps(state, ownProps) {
return {
...ownProps,
global: state.global,
gallery: state.gallery,
survey: state.survey,
survey_actual_answers: state.survey.survey_actual_answers,
survey_answers: state.survey.survey_answers,
survey_score_system: state.survey.survey_score_system,
survey_styles: state.survey.survey_styles,
survey_general_doc_data: state.survey.survey_general_doc_data,
isFetching: state.isFetching
};
}
function mapDispatchToProps(dispatch) {
return {
actions: bindActionCreators({
...globalActions,
...galleryActions,
...surveyActions
}, dispatch)
};
}
Does anyone know why this is happening? It's almost as if it's not calling componentDidMount at all.

This is happening because of how React works fundamentally. React is supposed to feel fast, fluent and snappy; the application should never get clogged up with http requests or asynchronous code. The answer is to use the lifecycle methods to control the DOM.
What does it mean when a component mounts?
It might be helpful to understand some of the React vocabularies a little better. When a component is mounted it is being inserted into the DOM. This is when a constructor is called. componentWillMount is pretty much synonymous with a constructor and is invoked around the same time. componentDidMount will only be called once after the first render.
componentWillMount --> render --> componentDidMount
How is that different than rerendering or updating?
Now that the component is in the DOM, you want to change the data that is displayed. When calling setState or passing down new props from the parent component a component update will occur.
componentWillRecieveProps --> shouldComponentUpdate-->componentWillUpdate
-->render-->componentDidUpdate
It is also good to note that http requests are usually done in componentDidMount and componentDidUpdate since these are places that we can trigger a rerender with setState.
So how do I get the data before the render occurs?
Well, there are a couple of ways that people take care of this. The first one would be to set an initial state in your component that will ensure that if the data from the http request has not arrived yet, it will not break your application. It will use a default or empty state until the http request has finished.
I usually don't like to have a loading modal, but sometimes it is necessary. For instance, when a user logs in you don't want to take them to a protected area of your site until they are finished authenticating. What I try to do is use that loading modal when a user logs in to front load as much data as I possibly can without affecting the user experience.
You can also make a component appear as loading while not affecting the user experience on the rest of the site. One of my favorite examples is the Airbnb website. Notice that the majority of the site can be used, you can scroll, click links, but the area under 'experiences' is in a loading state. This is the correct way to use React and is the reason why setState and HTTP requests are done in componentDidMount/componentDidUpdate.

Using setState in componentdidmount. This my code:
async componentDidMount() {
danhSachMon = await this.getDanhSachMon();
danhSachMon=JSON.parse(danhSachMon);
this.setState(danhSachMon);
}
render() {
return (
<View>
<FlatList
data={danhSachMon}
showsVerticalScrollIndicator={false}
renderItem={({ item }) =>
<View >
<Text>{item.title}</Text>
</View>
}
keyExtractor={(item, index) => index.toString()}
/>
</View>
)
}

componentWillMount is deprecated. Now you need to use "DidMount" and as soon as it finishes and changes the DOM on render, react will handle everything else.
Make sure you update and use the correct variables/state/props in the render.
componentDidMount() {
const { applicationId } = this.props;
if (applicationId) {
ApplicationService.getQuotesByApplicationId(applicationId).then((response) => {
const quotes = _.get(response, 'data.data');
this.setState({
quotes,
});
});
....
}
render() {
const quotes = _.get(this.state, 'quotes', null);
return (
<div className="application">
<h4 className="application__sub">Application</h4>
<h1 className="application__title">Quote Comparison</h1>
<div className="form-wrapper">
{this.renderQuotes(quotes)}
</div>
<div />
</div>
);
}
Here I get the quotes from the API and as soon as it finishes it set a variable in the state, then the render and react do their work.

Related

React Warning: Cannot update during an existing state transition in UseEffect

In React App, I am triggering API request / data load in useEffect() in the screen which shows loading indicator.
export default function LoadingScreen({ onLoaded}: {
onLoaded: (result: Result) => void;
}) {
useEffect(() => {
const getProductByCode = async () => {
const result = await getProduct();
onLoaded(result)
};
getProductByCode().catch(console.error);
});
return (…)
And when data is loaded, trying to switch the state here:
<LoadingScreen onLoaded={(result) => dispatch({ type: 'success', result })} />
It kind of works but I get this warning and I think it is not ok still
Warning: Cannot update during an existing state transition (such as within render). Render methods should be a pure function of props and state.
Can someone give me a bit more explanation on what is happening here (why warning is coming)? I thought useEffect() is called after rendering is done so invoking async dispatch after server response should be very much ok, no?
P.S. Added call stack. The line mentioned is const history = useHistory(); which does not make sense to me.
index-057ed940.js:1246 Warning: Cannot update during an existing state transition (such as within `render`). Render methods should be a pure function of props and state.
at App (https://localhost:8080/dist/App.js:69:19)
at Router (https://localhost:8080/_snowpack/pkg/react-router-dom.js:1683:30)
at BrowserRouter (https://localhost:8080/_snowpack/pkg/react-router-dom.js:2409:35)
at default
at InnerThemeProvider (https://localhost:8080/_snowpack/pkg/#material-ui/core.js:8806:17)
at ThemeProvider$1 (https://localhost:8080/_snowpack/pkg/#material-ui/core.js:4006:5)
at ThemeProvider (https://localhost:8080/_snowpack/pkg/#material-ui/core.js:8826:5)

Set initial state for material ui dialog

I have a MaterialUI dialog that has a few text fields, drop downs, and other things on it. Some of these elements need to be set to some value every time the dialog opens or re-opens. Others elements cannot be loaded until certain conditions exist (for example, user data is loaded).
For the 'resetting', I'm using the onEnter function. But the onEnter function doesn't run until entering (duh!)... but the render function, itself, still does - meaning any logic or accessing javascript variables in the JSX will still occur. This leaves the 'onEnter' function ill-equipped to be the place I set up and initialize my dialog.
I also can't use the constructor for setting/resetting this initial state, as the data I need to construct the state might not be available at the time the constructor loads (upon app starting up). Now, I could super-complicate my JSX in my render function and make conditionals for every data point... but that's a lot of overhead for something that gets re-rendered every time the app changes anything. (the material UI dialogs appear run the entire render function even when the 'open' parameter is set to false).
What is the best way to deal with initializing values for a material ui dialog?
Here is a super-dumbed-down example (in real life, imagine getInitialState is a much more complex, slow, and potentially async/network, function) - let's pretend that the user object is not available at app inception and is actually some data pulled or entered long after the app has started. This code fails because "user" is undefined on the first render (which occurs BEFORE the onEnter runs).
constructor(props) {
super(props);
}
getInitialState = () => {
return {
user: {username: "John Doe"}
}
}
onEnter = () => {
this.setState(this.getInitialState())
}
render() {
const { dialogVisibility } = this.props;
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle>
</Dialog> );
}
My first instinct was to put in an "isInitialized" variable in state and only let the render return the Dialog if "isInitialized" is true, like so:
constructor(props) {
super(props);
this.state = {
isInitialized: false
};
}
getInitialState = () => {
return {
user: {username: "John Doe"}
}
}
onEnter = () => {
this.setState(this.getInitialState(),
() => this.setState({isInitialized:true})
);
}
render() {
const { dialogVisibility } = this.props;
if(!this.state.isInitialized) {
return null;
}
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle>
</Dialog> );
}
As I'm sure you are aware... this didn't work, as we never return the Dialog in order to fire the onEnter event that, in turn, fires the onEnter function and actually initializes the data. I tried changing the !this.state.inInitialized conditional to this:
if(!this.state.isInitialized) {
this.onEnter();
return null;
}
and that works... but it's gives me a run-time warning: Warning: Cannot update during an existing state transition (such as withinrender). Render methods should be a pure function of props and state.
That brought me to a lot of reading, specifically, this question: Calling setState in render is not avoidable which has really driven home that I shouldn't be just ignoring this warning. Further, this method results in all the logic contained in the return JSX to still occur... even when the dialog isn't "open". Add a bunch of complex dialogs and it kills performance.
Surely there is a 'correct' way to do this. Help? Thoughts?
What you need conceptually is that when you are freshly opening the dialog, you want to reset some items. So you want to be able to listen for when the value of open changes from false to true.
For hooks, the react guide provides an example for keeping the "old" value of a given item with a usePrevious hook. It is then simply a matter of using useEffect.
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function MyDialog({ dialogVisibility }) {
const prevVisibility = usePrevious(dialogVisibility);
useEffect(() => {
// If it is now open, but was previously not open
if (dialogVisibility && !prevVisibility) {
// Reset items here
}
}, [dialogVisibility, prevVisibility]);
return <Dialog open={dialogVisibility}></Dialog>;
}
The same thing can be achieved with classes if you use componentDidUpdate and the previousProps parameter it receives.
export class MyDialog extends Component {
public componentDidUpdate({ dialogVisibility : prevVisibility }) {
const { dialogVisibility } = this.props;
if (dialogVisibility && !prevVisibility) {
// Reset state here
}
}
public render() {
const { dialogVisibility } = this.props;
return <Dialog open={dialogVisibility}></Dialog>;
}
}
You should use componentDidUpdate()
This method is not called for the initial render
Use this as an opportunity to operate on the DOM when the component has been updated
If you need data preloaded before the dialog is opened, you can use componentDidMount():
is invoked immediately after a component is mounted (inserted into the tree)
if you need to load data from a remote endpoint, this is a good place to instantiate the network request
React guys added the useEffect hook exactly for cases like the one you are describing, but you would need to refactor to a functional component.
Source: https://reactjs.org/docs/hooks-effect.html
This can be solved by doing leaving the constructor, getInitialState, and onEnter functions as written and making the following addition of a ternary conditional in the render function :
render() {
const { dialogVisibility } = this.props;
return (
<Dialog open={dialogVisibility} onEnter={this.onEnter}>
{this.state.isInitialized && dialogVisibility ?
<DialogTitle>
Hi, {this.state.user.username}
</DialogTitle> : 'Dialog Not Initialized'}
</Dialog> );
)}
It actually allows the dialog to use it's "onEnter" appropriately, get the right transitions, and avoid running any extended complex logic in the JSX when rendering while not visible. It also doesn't require a refactor or added programming complexity.
...But, I admit, it feels super 'wrong'.

Warning: setState(...): Cannot update during an existing state transition with redux/immutable

I am getting the error
Warning: setState(...): Cannot update during an existing state transition (such as within render or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to componentWillMount.
I found the cause to be
const mapStateToProps = (state) => {
return {
notifications: state.get("notifications").get("notifications").toJS()
}
}
If I do not return notifications there it works. But why is that?
import {connect} from "react-redux"
import {removeNotification, deactivateNotification} from "./actions"
import Notifications from "./Notifications.jsx"
const mapStateToProps = (state) => {
return {
notifications: state.get("notifications").get("notifications").toJS()
}
}
const mapDispatchToProps = (dispatch) => {
return {
closeNotification: (notification) => {
dispatch(deactivateNotification(notification.id))
setTimeout(() => dispatch(removeNotification(notification.id)), 2000)
}
}
}
const NotificationsBotBot = connect(mapStateToProps, mapDispatchToProps)(Notifications)
export default NotificationsBotBot
import React from "react"
class Notifications extends React.Component {
render() {
return (
<div></div>
)
}
}
export default Notifications
UPDATE
On further debugging I found that, the above may not be the root cause after all, I can have the notifications stay but I need to remove dispatch(push("/domains")) my redirect.
This is how I login:
export function doLogin (username, password) {
return function (dispatch) {
dispatch(loginRequest())
console.log("Simulated login with", username, password)
setTimeout(() => {
dispatch(loginSuccess(`PLACEHOLDER_TOKEN${Date.now()}`))
dispatch(addNotification({
children: "Successfully logged in",
type: "accept",
timeout: 2000,
action: "Ok"
}))
dispatch(push("/domains"))
}, 1000)
}
}
I find that the dispatch causes the warning, but why? My domains page have nothing much currently:
import {connect} from "react-redux"
import DomainsIndex from "./DomainsIndex.jsx"
export default connect()(DomainsIndex)
DomainsIndex
export default class DomainsIndex extends React.Component {
render() {
return (
<div>
<h1>Domains</h1>
</div>
)
}
}
UPDATE 2
My App.jsx. <Notifications /> is what displays the notifications
<Provider store={store}>
<ConnectedRouter history={history}>
<Layout>
<Panel>
<Switch>
<Route path="/auth" />
<Route component={TopBar} />
</Switch>
<Switch>
<Route exact path="/" component={Index} />
<Route path="/auth/login" component={LoginBotBot} />
<AuthenticatedRoute exact path="/domains" component={DomainsPage} />
<AuthenticatedRoute exact path="/domain/:id" component={DomainPage} />
<Route component={Http404} />
</Switch>
<Notifications />
</Panel>
</Layout>
</ConnectedRouter>
</Provider>
Your dispatch(push('/domains')) comes along other dispatches that set the state for a connected component (presumably one that cares about notifications) that gets remounted/unmounted after the push takes effect.
As a workaround and proof of concept, try defering the dispatch(push('/domains')) call with a nested zero-second setTimeout. This should execute the push after any of the other actions finish (i.e. hopefully a render):
setTimeout(() => dispatch(push('/domains')), 0)
If that works, then you might want to reconsider your component structure. I suppose Notifications is a component you want to mount once and keep it there for the lifetime of the application. Try to avoid remounting it by placing it higher in the component tree, and making it a PureComponent (here are the docs). Also, if the complexity of your application increases, you should consider using a library to handle async functionality like redux-saga.
Even though this warning usually appears because of a misplaced action call (e.g. calling an action on render: onClick={action()} instead of passing as a lambda: onClick={() => action()}), if your components look like you've mentioned (just rendering a div), then that is not the cause of the problem.
I had this issue in the past, and managed to resolve it using redux-batched-actions.
It's very useful for use-cases like yours when you dispatch multiples actions at once and you're unsure of when the updates will get triggered, with this, there will be only one single dispatch for multiple actions. In your case it seems that the subsequent addNotification is fine, but the third one is too much, maybe because it interacts with the history api.
I would try to do something like (assuming your setTimeout will be replaced by an api call of course):
import { batchActions } from 'redux-batched-actions'
export function doLogin (username, password) {
return function (dispatch) {
dispatch(loginRequest())
console.log("Simulated login with", username, password)
setTimeout(() => {
dispatch(batchActions([
loginSuccess(`PLACEHOLDER_TOKEN${Date.now()}`),
addNotification({
children: "Successfully logged in",
type: "accept",
timeout: 2000,
action: "Ok"
}),
push("/domains")
]))
}, 1000)
}
}
Note that you'll have to download the package and enableBatching your reducer in your store creation.
The reason this is happening is when are you calling the doLogin, if you are calling it from within a constructor. If this is the case try moving it to componentWillMount although you should be calling this method from a button or enter hit in the login form.
This have been documented in constructor should not mutate If this is not the root of the problem you mind commented each line in doLogin to know exactly which line giving the state problem, my guess would be either the push or the addNotification
There is not enough info to give a certain answer. But what is for sure is that this warning is raised when you tries to setState inside render method.
Most often it happens when you are calling your handler functions instead of passing them as props to child Component. Like it happened here or here.
So my advice is to double check which Components are being rendered on your /domains route and how you are passing onChange, onClick, etc. handlers to them and to their children.
In a react component when you call setState({...}), it causes the component to re-render and call all the life cycle methods which are involved in re-rendering of the component and render method is one of them
When in render if you call setState({...}) it will cause a re-render and render will be called again and again hence this will trigger an infinite loop inside the javascript eventually leading to crashing of the app
Hence, not only in render but in any life-cycle method which is a part of re-render process setState({...}) method shouldn't be called.
In your case the code might be triggering an update in the redux state while the code is still rendering and hence this causes re-render and react shows error

Updating component state in React-Redux with API calls

I'm trying to set up a React app where clicking a map marker in one component re-renders another component on the page with data from the database and changes the URL. It works, sort of, but not well.
I'm having trouble figuring out how getting the state from Redux and getting a response back from the API fit within the React life cycle.
There are two related problems:
FIRST: The commented-out line "//APIManager.get()......" doesn't work, but the hacked-together version on the line below it does.
SECOND: The line where I'm console.log()-ing the response logs infinitely and makes infinite GET requests to my database.
Here's my component below:
class Hike extends Component {
constructor() {
super()
this.state = {
currentHike: {
id: '',
name: '',
review: {},
}
}
}
componentDidUpdate() {
const params = this.props.params
const hack = "/api/hike/" + params
// APIManager.get('/api/hike/', params, (err, response) => { // doesn't work
APIManager.get(hack, null, (err, response) => { // works
if (err) {
console.error(err)
return
}
console.log(JSON.stringify(response.result)) // SECOND
this.setState({
currentHike: response.result
})
})
}
render() {
// Allow for fields to be blank
const name = (this.state.currentHike.name == null) ? null : this.state.currentHike.name
return (
<div>
<p>testing hike component</p>
<p>{this.state.currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
params: state.hike.selectedHike
}
}
export default connect(stateToProps)(Hike)
Also: When I click a link on the page to go to another url, I get the following error:
"Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op."
Looking at your code, I think I would architect it slightly differently
Few things:
Try to move the API calls and fetch data into a Redux action. Since API fetch is asynchronous, I think it is best to use Redux Thunk
example:
function fetchHikeById(hikeId) {
return dispatch => {
// optional: dispatch an action here to change redux state to loading
dispatch(action.loadingStarted())
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err);
// if you want user to know an error happened.
// you can optionally dispatch action to store
// the error in the redux state.
dispatch(action.fetchError(err));
return;
}
dispatch(action.currentHikeReceived(response.result))
});
}
}
You can map dispatch to props for fetchHikeById also, by treating fetchHikeById like any other action creator.
Since you have a path /hike/:hikeId I assume you are also updating the route. So if you want people to book mark and save and url .../hike/2 or go back to it. You can still put the the fetch in the Hike component.
The lifecycle method you put the fetchHikeById action is.
componentDidMount() {
// assume you are using react router to pass the hikeId
// from the url '/hike/:hikeId'
const hikeId = this.props.params.hikeId;
this.props.fetchHikeById(hikeId);
}
componentWillReceiveProps(nextProps) {
// so this is when the props changed.
// so if the hikeId change, you'd have to re-fetch.
if (this.props.params.hikeId !== nextProps.params.hikeId) {
this.props.fetchHikeById(nextProps.params.hikeId)
}
}
I don't see any Redux being used at all in your code. If you plan on using Redux, you should move all that API logic into an action creator and store the API responses in your Redux Store. I understand you're quickly prototyping now. :)
Your infinite loop is caused because you chose the wrong lifecycle method. If you use the componentDidUpdate and setState, it will again cause the componentDidUpdatemethod to be called and so on. You're basically updating whenever the component is updated, if that makes any sense. :D
You could always check, before sending the API call, if the new props.params you have are different than the ones you previously had (which caused the API call). You receive the old props and state as arguments to that function.
https://facebook.github.io/react/docs/react-component.html#componentdidupdate
However, if you've decided to use Redux, I would probably move that logic to an action creator, store that response in your Redux Store and simply use that data in your connect.
The FIRST problem I cannot help with, as I do not know what this APIManager's arguments should be.
The SECOND problem is a result of you doing API requests in "componentDidUpdate()". This is essentially what happens:
Some state changes in redux.
Hike receives new props (or its state changes).
Hike renders according to the new props.
Hike has now been updated and calls your "componentDidUpdate" function.
componentDidUpdate makes the API call, and when the response comes back, it triggers setState().
Inner state of Hike is changed, which triggers an update of the component(!) -> goto step 2.
When you click on a link to another page, the infinite loop is continued and after the last API call triggered by an update of Hike is resolved, you call "setState" again, which now tries to update the state of a no-longer-mounted component, hence the warning.
The docs explain this really well I find, I would give those a thorough read.
Try making the API call in componentDidMount:
componentDidMount() {
// make your API call and then call .setState
}
Do that instead of inside of componentDidUpdate.
There are many ways to architect your API calls inside of your React app. For example, take a look at this article: React AJAX Best Practices. In case the link is broken, it outlines a few ideas:
Root Component
This is the simplest approach so it's great for prototypes and small apps.
With this approach, you build a single root/parent component that issues all your AJAX requests. The root component stores the AJAX response data in it's state, and passes that state (or a portion of it) down to child components as props.
As this is outside the scope of the question, I'll leave you to to a bit of research, but some other methods for managing state and async API calls involved libraries like Redux which is one of the de-facto state managers for React right now.
By the way, your infinite calls come from the fact that when your component updates, it's making an API call and then calling setState which updates the component again, throwing you into an infinite loop.
Still figuring out the flow of Redux because it solved the problem when I moved the API request from the Hike component to the one it was listening to.
Now the Hike component is just listening and re-rendering once the database info catches up with the re-routing and re-rendering.
Hike.js
class Hike extends Component {
constructor() {
super()
this.state = {}
}
componentDidUpdate() {
console.log('dealing with ' + JSON.stringify(this.props.currentHike))
}
render() {
if (this.props.currentHike == null || undefined) { return false }
const currentHike = this.props.currentHike
return (
<div className="sidebar">
<p>{currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
currentHike: state.hike.currentHike,
}
}
And "this.props.currentHikeReceived()" got moved back to the action doing everything in the other component so I no longer have to worry about the Hikes component infinitely re-rendering itself.
Map.js
onMarkerClick(id) {
const hikeId = id
// Set params to be fetched
this.props.hikeSelected(hikeId)
// GET hike data from database
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err)
return
}
this.props.currentHikeReceived(response.result)
})
// Change path to clicked hike
const path = `/hike/${hikeId}`
browserHistory.push(path)
}
const stateToProps = (state) => {
return {
hikes: state.hike.list,
location: state.newHike
}
}
const dispatchToProps = (dispatch) => {
return {
currentHikeReceived: (hike) => dispatch(actions.currentHikeReceived(hike)),
hikesReceived: (hikes) => dispatch(actions.hikesReceived(hikes)),
hikeSelected: (hike) => dispatch(actions.hikeSelected(hike)),
locationAdded: (location) => dispatch(actions.locationAdded(location)),
}
}

Why componentWillMount is called after rendering?

I am working with React and I am trying to understand the lifecycle. I am doing a componentWillMount method in order to get the props I need before the render occurs. I need to know how to update the state when the view loads.
All I am trying to do is a GET request in order to get a list of dealers for a Casino Game. Basically, I am missing 1 or 2 steps which are for render the dealers's list in the DOM
I will show what I am doing with my code and after that I will explain what I want
Actions part
getDealerActions.js
class GetDealersActions {
constructor () {
this.generateActions('dealerDataSuccess', 'dealerDataFail');
}
getDealers (data) {
const that = this;
that.dispatch();
axios.get('someroute/get-dealers/get-dealers')
.then(function success (response) {
that.actions.dealerDataSuccess({...response.data});
})
}
};
then we move to the stores
getDealersStore.js
class GetDealersStore {
constructor () {
this.state = {
dealerData : null,
};
}
#bind(GetDealersActions.dealerDataSuccess)
dealerDataSuccess (data) {
this.setState({
dealerData : data,
});
console.log(this.state.dealerData);
}
}
in this case that console.log(this.state.dealerData); returns something like this which is exactly what I need
Object {dealersData: Array[3]}
the problems comes in the component part, honestly because I don't know how to handle the data here
#connectToStores
export default class Dealers extends Component {
static contextTypes = {
router : React.PropTypes.func,
}
constructor (props) {
super(props);
this.state = {}
}
static getStores () {
return [ GetDealersStore ];
}
static getPropsFromStores () {
return GetDealersStore.getState();
}
componentWillMount () {
console.log('###', this.props);
GetDealersActions.getDealers();
}
render () {
console.log('>>>', this.props);
let content;
if (this.state.dealerData) {
content = this.state.dealerData.map((item) => {
return <div key={item.CardId}>{item}</div>;
});
} else {
content = <div>Loading . . .</div>;
}
return (
<div>
<div>{content}</div>
</div>
);
}
}
all I get here <div>{content}</div> is Loading . . . because this.state is coming like this Object {}
A weird situation I am getting here, is that this view is rendering twice, the 1st time is rendering, and the console.log('>>>', this.props); returns this >>> Object {params: Object, query: Object} and the second time it renders, fires this >>> Object {params: Object, query: Object, dealerData: Object} which is what I need.
So, why componentWillMount is waiting the render method in order to get fired ?
It's not weird at all. componentWillMount will fire before render, and in the first-pass you are invoking an action to get the dealers GetDealersActions.getDealers(); which is basically an async command. Since it is async, the component will render once before it gets data, and then again after the store publishes a changed event, which will re-trigger rendering.
Here is an approximation of the sequence of actions happening in your example:
componentWillMount invokes getDealers command (which is async)
initial render with default component state
Async operation completed in action creator and store is set with dealer data
store publishes a changed event, which re-triggers rendering
second render invoked with the dealer data in component state.
The problem is that React will run it's lifecycle methods in a certain sequence, not caring about you invoking some async method. So basically you don't have a way to stop rendering just because you invoked a command to get the dealers. That is a limitation of react (or a feature), which surfaces when combined with async programming and you should accept it as is.
If you accept the fact that React will render twice, you can utilize that in your favor, so on first render you could just show a loading indicator (e.g. a spinning wheel) and when the data loads you just display it in the second render.
However, if you are not convinced and still want to avoid double-rendering in the initial load, you could do prefetching of the data before you mount the application component, which would ensure that initial data is loaded in the store before the first render, which would mean that you wouldn't have to invoke getDealers in componentWillMount since the data would already be in the store on the first render.
As a reminder, double-rendering is not a significant performance problem, like it would be in Angular.js or Ember.js, since React is very efficient at DOM manipulation, but it could produce some UX issues if not handled properly.

Categories