Flux + React.js - Callback in actions is good or bad? - javascript

Let me explain the problem that I've faced recently.
I have React.js + Flux powered application:
There is a list view of articles (NOTE: there are multiple of of different lists in the app) and article details view inside it.
But there is only one API endpoint per each list which returns array of articles.
In order to display the details I need to find article by id in array. That works pretty fine. I trigger action which makes request to server and propagates store with data, when I go to details screen then I just get the necessary article from that array in store.
When user lands on article details view before list (stores are empty) then I need to make a request.
Flow looks like: User loads details view -> component did mount -> stores are empty -> rendered empty -> fetchArticles action is triggered -> request response is 200 -> stores now have list of articles -> component did update -> rendered with data successfully
Component could look as follows:
let DetailsComponent = React.createClass({
_getStateFromStores() {
let { articleId } = this.getParams();
return {
article: ArticleStore.getArticle(articleId)
};
},
componentDidMount() {
// fire only if user wasn't on the list before
// stores are empty
if (!this.state.article) {
ArticleActions.fetchArticles('listType');
}
},
render() {
return <ArticleDetails article={this.state.article} />;
}
});
The interesting part comes next:
Now I need to make another request to server but request options depend on the article details. That's why I need to make second request after the first one on the details view.
I've tried several approaches but all of them look ugly. I don't like calling actions from stores that makes stores too complicated. Calling action inside action in this case doesn't work well because I will need to find article from store inside that action.
Solution (?!)
What I've came up with is to use callback in action inside component and it feels much more cleaner:
let DetailsComponent = React.createClass({
_getStateFromStores() {
let { articleId } = this.getParams();
return {
article: ArticleStore.getArticle(articleId)
};
},
componentDidMount() {
if (!this.state.article) {
ArticleActions.fetchArticles('listType', () => {
this._requestAdditionalData();
});
}
this._requestAdditionalData();
},
_requestAdditionalData() {
if (this.state.article) {
ArticleActions.fetchAdditional(this.state.article.property);
}
},
render() {
return <ArticleDetails article={this.state.article} />;
}
});
What's your input?

Consider move the second call to get a detail article to the ArticleDetails component componentDidMount() life cycle method.
So if the article is not set, do not render the ArticleDetails component at all by return null / false.

Related

Sending a post request to jsonplaceholder fails in React Redux

I made a simple React Redux application that fetches a list of posts from jsonplaceholder and within it is a form that allows users to send a POST request. When I send a post request according to Redux DevTools extension it is added successfully marked as post number 101. Here is its snapshot
but the problem is after clicking the submit button 3 times it shows up on the screen.
The first two clicks show neither the title nor its body but it starts showing up on the third click.
This is Posts.jsx file and here is how I used componentDidUpdate to update the component after post request.
class Posts extends Component {
componentDidMount(){
this.props.fetchPosts();
}
componentDidUpdate(nextProps) {
if (nextProps.newPost) {
this.props.posts.unshift(nextProps.newPost);
}
}
renderPosts(){ // cutted for brevity }
render() {
return (
{this.renderPosts()}
)
}
}
const mapStateToProps = (state) => {
return {
posts: state.posts.items,
newPost: state.posts.item,
}
}
export default connect(mapStateToProps, { fetchPosts })(Posts);
Here is its GitHub link repository.
The only error I am getting is the below error.
index.js:1 Warning: Each child in a list should have a unique "key" prop.
I don't believe this has anything to do with rendering the new post, but I already specified a "key" while looping through components.
What I am doing wrong during the course of this post request? Thank You.
You are using the wrong lifeCycle method. in order to get the nexProps you have to use componentWillReceiveProps instead of componentDidUpdate.
componentDidUpdate will give you the previous Props and previous State.
componentWillReceiveProps(nextProps) {
if (nextProps.newPost) {
this.props.posts.unshift(nextProps.newPost);
}
}
The above snippet should work.
But this method is deprecated. react introduced an alternative (kind of) of this. which is called getDerivedStateFromProps. The problem is it is a static method and you can't access previous props (this.props) inside this method.
If you need it you did something wrong as it is an anti-pattern.

Only make API call when data in Vuex store is "stale" or not present

I am using Vuex for state management in my VueJS 2 application. In the mounted property of my component in question I dispatch an action...
mounted: function () {
this.$store.dispatch({
type: 'LOAD_LOCATION',
id: this.$route.params.id
});
}
...and this action uses axios to make an API call and get that location's details.
LOAD_LOCATION: function ({ commit }, { id }) {
axios.get(`/api/locations/${id}`).then((response) => {
commit('SET_LOCATION', { location: response.data })
}, err => {
console.log(err);
});
}
The mutation looks like so:
SET_LOCATION: (state, { location }) => {
state.locations.push(location);
}
This makes complete sense the first time this location is navigated to. However, let's say a user navigates to /locations/5 then navigates elsewhere in the app and returns to /locations/5 a few minutes later. Would it be a good idea to check for the location in state.locations and only make the API call if this location is not present? Or even better, to check the "staleness" of the location data and only make the API call to refresh the data after a certain period has passed?
Edit: Is there a pattern that is typically followed for these cases with Vuex? It seems to be a common case, but I'm not sure if jamming the logic to check for presence/staleness in the action is a solid approach.
Personally, I think it would be an excellent idea to do a check within the action to see if the data exists and set a timestamp when the data is received, then on subsequent calls it could determine if the data exits/is stale and act accordingly. That would speed up repeat visits and also save mobile users' data a bit.

ReactJS: How to handle component state when it's based on AJAX?

Summary: New to ReactJS and I'm trying to figure out the best way to update a component when it's state depends on a remote API (i.e. keep component state in sync with remote database via AJAX API).
Example Use Case: Think of a product inventory where clicking a button adds a product to your cart and decrements the inventory by 1. Every time the user clicks it initiates an AJAX request and then upon completion of the request, the component re-renders with the new product inventory by calling setState().
Problem: I've ran into an issue where because both setState() and the AJAX request are asynchronous, the component becomes out of the sync with the server. For example if you click really quickly you can initiate more than one AJAX request for a single product ID because the component's state has not yet updated to reflect that the product ID is no longer in inventory. I have a simple example below to illustrate the concept:
Inadequate Solution: This could be handled on the server side by sending an error back if the client request a product that is no longer in inventory, however I'm really looking for the best way to handle this common scenario in ReactJS on the client side and to make sure I'm understanding the best way to handle component state.
Component extends React.Component {
constructor(props) {
super(props);
this.state = {
clicksLeft: 0,
};
}
componentDidMount() {
//getClicksLeft is async and takes a callback, think axios/superagent
getClicksLeft((response) => {
this.setState(response);
});
}
btnClicked = () => {
//This may appear redundant/useless but
//imagine sending an element in a list and then requesting the updated
//list back
const data = {clicks: this.state.clicksLeft--};
decrementClicksLeft(data, () => {
getClicksLeft((response) => {
this.setState(response);
});
}
}
render() {
<button onClick={this.btnClicked}>Click me {this.state.clicksLeft} times</button>
}
}
Is there any reason to have to call getClicksLeft when the button is clicked? You have already called it when the component is mounted and then anytime the button is clicked you just decrement that number by one.
btnClicked = () => {
if (this.state.clicksLeft > 0) {
decrementClicksLeft();
this.setState({clicksLeft: this.state.clicksLeft - 1});
}
}
This would work if there is only one user trying to buy stuff at a time. Otherwise you could also check the amount left before making the purchase.
btnClicked = () => {
getClicksLeft((response) => {
if (response > 0) {
decrementClicksLeft();
this.setState({clicksLeft: this.state.clicksLeft - 1});
}
});
}
This way if there are no clicks left, nothing happens.
The most basic solution would be to disable the button while you wait for the response to come back:
(I've also made your code simpler.)
Component extends React.Component {
constructor(props) {
super(props);
// Initial state
this.state = {
clicksLeft: 0, // No clicks are availabe
makeRequest: false, // We are not asking to make a request to the server
pendingTransaction: false, // There is no current request out to the server
};
}
componentDidMount() {
// Initial load completed, so go get the number of clicks
this._getClicksRemaining();
}
// Called whenever props/state change
// NOT called for the initial render
componentWillUpdate(nextProps, nextState) {
// If there is no existing request out to the server, AND if the next
// state is asking us to make a request (as set in _handleButtonClick)
// then go make the request
if (!this.state.pendingTransaction && nextState.makeRequest) {
const data = {
clicks: this.state.clicksLeft--,
};
// decrementClicksLeft is async
decrementClicksLeft(data, () => this._getClicksRemaining());
// First fire off the async decrementClicksLeft request above, then
// tell the component that there is a pending request out, and that it
// is not allowed to try and make new requests
// NOTE this is the one part of the code that is vulnerable to your
// initial problem, where in theory a user could click the button
// again before this setState completes. However, if your user is able
// to do that, then they are probably using a script and you shouldn't
// worry about them. setState/render is very fast, so this should be
// more than enough protection against human clicking
this.setState({
makeRequest: false,
pendingTransaction: true,
});
}
}
_getClicksRemaining() {
// getClicksLeft is async
getClicksLeft((response) => {
// Here we are inside of the callback from getClicksLeft, so we
// know that it has completed. So, reset our flags to show that
// there is no request still pending
const newState = Object.assign(
{
pendingTransaction: false,
},
response,
);
this.setState(newState);
});
}
// The button was clicked
_handleButtonClick = () => {
if (!this.state.pendingTransaction) {
// If there isn't a request out to the server currently, it's safe to
// make a new one. Setting state here will cause `componentWillUpdate`
// to get called
this.setState({
makeRequest: true,
});
}
}
render() {
// Disable the button if:
// * there are no clicks left
// * there is a pending request out to the server
const buttonDisabled = ((this.state.clicksLeft === 0) || this.state.pendingTransaction);
return (
<button
disabled={buttonDisabled}
onClick={this._handleButtonClick}
>
Click me {this.state.clicksLeft} times
</button>
);
}
}
After spending some time with react-redux, redux-thunk and redux-pack I decided to go with something simpler: react-refetch. I didn't really need the complexities of redux as I am only doing post and get operations on lists. I also need some simple side effects like when I do a post, I need to update multiple lists (which is achieved through andThen() in react-refetch).
This solution has much less boiler plate and works great for small projects. The core reason to choose this project over react-redux can be summarized in this quote from heroku's blog entry:
Looking around for alternatives, Redux was the Flux-like library du jour, and it did seem very promising. We loved how the React Redux bindings used pure functions to select state from the store and higher-order functions to inject and bind that state and actions into otherwise stateless components. We started to move down the path of standardizing on Redux, but there was something that felt wrong about loading and reducing data into the global store only to select it back out again. This pattern makes a lot of sense when an application is actually maintaining client-side state that needs to be shared between components or cached in the browser, but when components are just loading data from a server and rendering it, it can be overkill.
1: https://github.com/heroku/react-refetch
2: https://engineering.heroku.com/blogs/2015-12-16-react-refetch/

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)),
}
}

Initial data loading ReactJS

I would like to perform some initial data loading when my first route is rendered (for example, i want to load a list of news articles)
I made a component called News.js which renders the articles. The problem i'm experiencing with the FLUX model is where to load this initial data.
The service i have made to load the data is the following:
import request from 'superagent';
class NewsService {
fetchArticles(callback) {
request
.get('http://localhost/articles')
.accept('json')
.end(function(err, res){
console.log(err);
var result = JSON.parse(res.text);
callback(result);
})
}
}
export default new NewsService ();
This service has to be called somewhere. According to the ReactJS documentation i would perform this operation like this:
export default class News extends React.Component {
constructor() {
super();
this.state = {
_articles: []
}
}
componentDidMount() {
NewsService.fetchProjects(function(articles){
// load articles in the state
this.setState({_articles: _articles})
});
}
render() {
return (
<section>
<h1>Articles</h1>
<ul>
{this.state.articles.map((article) => {
<li>{article.title}</li>
})}
</ul>
</section>
)
}
}
Now my question is, isn't this against the flux principle? Shouldn't the data be called as an Action which then stores it's data in a store such as NewsStore?
Should an action be like the following:
var NewsActions = {
load: function() {
NewsService.fetchProjects(function(articles){
// store the articles in the NewsStore and dispatch afterwards
});
},
create: function(project) {
AppDispatcher.dispatch({
actionType: NewsConstants.ARTICLE_CREATE,
project: project
});
},
update: function(id, project) {
AppDispatcher.dispatch({
actionType: NewsConstants.ARTICLE_UPDATE,
id: id,
project: project
})
},
destroy: function() {
AppDispatcher.dispatch({
actionType: NewsConstants.ARTICLE_DESTROY,
id: id
})
}
};
export default NewsActions;
In the Chat-app example of reactjs they provide an API call example. However this API call is called on the application start up (in app.js) which is not applicable in my scenario as i would like to use routings. Would i load the data in the router then? (I am using react-router)
Any feedback regarding this matter or improvements of this code is more than welcome.
EDIT
isn't this against the flux principle?
Maybe, maybe not. Seems like Flux is pretty flexible. From what I understand, it's more of a framework of principles rather than a strict "protocol". It's hard to say, but it appears that both example you've given will work. Like you said, according to the docs, they recommend fetching the data in componentDidMount:
componentDidMount: function() {
$.get(this.props.source, function(result) {
// ...
However, in your example, you're simply moving that API call into a service, which can then interact with the store/dispatcher, etc., in order to be utilized across the entire application.
So what you've done is moved a nice chunk of your application logic to, essentially, its own module (well, a module that is a part of your dispatchers). It appears that it will meet your needs: it can be used across your app, and it can be pulled out or plugged back in as you see fit. I don't see anything wrong with it. Could it be against some principle of Flux? Maybe, maybe not. I doubt it matters, though.
ORIGINAL
I'm not well-versed in Flux architecture, but looking at one of Facebook's examples in their GitHub repo (specifically, Line 24 of TodoMVC):
function getTodoState() {
return {
allTodos: TodoStore.getAll(),
areAllComplete: TodoStore.areAllComplete()
};
}
Their example doesn't show how TodoStore interacts with the server, but it does look like for their initial state, they're simply querying the todos in the store, and then for changes, their listening for and emitting events.
So as far as getting the initial state, it looks like their example shows querying the store directly. Obviously since the time they made that example and now, there may have been changes, but it may be worth investigating some examples in the Flux repo to get an idea of how it was designed.

Categories