I have a simple React button component that when clicked should retrieve and download data on the client browser. The problem I am experiencing is that the download is triggered and the csv file downloaded before the data is passed into the href.
Here is my component:
import { Component } from 'react';
import { connect } from 'react-redux';
import { PropTypes } from 'prop-types';
import { ManageUsersSelectors } from 'selectors/Users';
import { BatchRoleActions } from 'actions/Users';
class UsersExportButton extends Component {
constructor() {
super();
this.state = {
users: ''
};
}
getUsers(){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users;
});
}
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'} onClick={() => this.getUsers()} return true>Export Csv</a>
</div>
);
}
}
function mapStateToProps(state) {
const userIds = ManageUsersSelectors.batchUserIdsSelector(state);
return {
userIds: userIds
};
}
UsersExportButton.propTypes = {
text: PropTypes.string.isRequired,
data: PropTypes.array
};
export default connect(mapStateToProps)(UsersExportButton);
How can I get the getUsers()/onClick function to complete the data retrieval step before downloading?
When i debug my code I can see that the getUsers function returns data - however after the download is triggered
Make sure to bind this to your functions. In your constructor you can do:
constructor() {
super();
this.state = {
users: ''
};
this.getUsers = this.getUsers.bind(this);
}
or you can use the bind this function:
getUsers = () => {
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
this.setState({ users: users});
return this.state.users; // This should be removed, you can use this.state.users throughout this component.
});
}
Why not get the user data in the componentDidMount lifecycle method? It doesn't look like it needs to be called onClick.
{
// ...
componentDidMount() {
this.getUsers();
}
// ...
render() {
return (
<div className="roles-export-button">
<a className="button button-default" href={this.state.users} download={'roles.csv'}>Export Csv</a>
</div>
)
}
}
How about handling the default "link" behaviour manually to get more control? Also you should probably try to access state after setState has been executed via its callback.
e.g.
getUsers(cb){
const { userIds } = this.props;
BatchRoleActions.getAllRoleUsers(userIds)
.then((users) => {
// note the callback of setState which is invoked
// when this.state has been set
this.setState({ users: users }, cb);
});
}
const handleClick = () => {
this.getUsers(() => {
window.open(this.state.whatever)
})
}
<span onClick={handleClick}>Export Csv</span>
Related
TLDR: I am building a React router app, I trying to update the state of my component through a history listener, this listener works fine I put a console.log and I can see it, but the state of my component is not changing, I can see this with the React chrome extension and my component is not updating.
`
import React from "react";
import { withRouter } from "react-router-dom";
import { styles } from './Styles';
import { url } from './App';
class Searchresults extends React.Component {
constructor(props) {
super(props);
this.state = {
searchResults : []
}
}
async fetchResults(endpoint) {
try {
const response = await fetch(endpoint);
if (response.ok) {
const rJson = await response.json();
return rJson;
}
} catch (err) {
console.log(err);
}
}
componentDidMount() {
this.searchUpdate();
this.unlisten = this.props.history.listen((location, action) => {
console.log("it works!");
this.searchUpdate();
})
}
searchUpdate = () => {
const { location } = this.props;
const params = new URLSearchParams(location);
const query = params.get("search");
const name = query.replace("?name", "s");
const endpoint = url + "&" + name;
this.fetchResults(endpoint).then(response => {
return response['Search'].map(item => {
return { title: item['Title'], poster: item['Poster'], id: item['imdbID'] }
})
}).then(response => {
this.setState({
searchResults : response
})
});
}
render() {
return (
<div style={styles.movieList}>
<ul>
{
!this.state.searchResults? 'Loading' : this.state.searchResults.map((item, index) => {
return (<li key={index}>
<a href={'/moviepage?id=' + item.id}>{item.title}</a><br />
<img src={item.poster} alt="Movie poster"
style={{ width: "6rem", height: "auto" }} />
</li>)
})
}
</ul>
</div>
);
}
}
export default withRouter(Searchresults);
`
I am trying to update the state with a method searchUpdate, then this method is called in componentDidMount, here works fine, then when the URL changes, the history.listen triggers and searchUpdate is fired again, and everything seems to work except the change of the state of my component.
The first .then function in your searchResult function doesn't return a promise, so there is no need to use another .then. Just put the setState call in the same block:
this.fetchResults(endpoint).then(response => {
const searchResults = response['Search'].map(item => {
return { title: item['Title'], poster: item['Poster'], id: item['imdbID'] }
});
this.setState({searchResults})
});
I am new to MST and is having a hard time finding more examples with async actions. I have an api that will return different data depending on the params you pass to it. In this case, the api can either return an array of photos or tutorials. I have set up my initial values for the store like so:
data: {
photos: [],
tutorials: []
}
Currently, I am using applySnapshot to update the store and eventually, that will trigger a re-render of my React component. In order to display both photos and tutorials, I need to call the api twice (Once with the params for photos and the second time for tutorials). I am running into an issue where the snapshot from the first update shows that photos and tutorials have the same values and only on the second update, do I get the correct values. I am probably misusing applySnapshot to re-render my React components. I would like to know the better/proper way of doing this. What is the best way to re-render my React components after the api has yielded a repsonse. Any suggestions are much appreciated
I have set up my store like this:
import { RootModel } from '.';
import { onSnapshot, getSnapshot, applySnapshot } from 'mobx-state-tree';
export const setupRootStore = () => {
const rootTree = RootModel.create({
data: {
photos: [],
tutorials: []
}
});
// on snapshot listener
onSnapshot(rootTree, snapshot => console.log('snapshot: ', snapshot));
return { rootTree };
};
I have created the following model with an async action using generators:
import {types,Instance,applySnapshot,flow,onSnapshot} from 'mobx-state-tree';
const TestModel = types
.model('Test', {
photos: types.array(Results),
tutorials: types.array(Results)
})
.actions(self => ({
fetchData: flow(function* fetchData(param) {
const results = yield api.fetch(param);
applySnapshot(self, {
...self,
photos: [... results, ...self.photos],
tutorials: [... results, ...self.tutorials]
});
})
}))
.views(self => ({
getPhoto() {
return self.photos;
},
getTutorials() {
return self.tutorials;
}
}));
const RootModel = types.model('Root', {
data: TestModel
});
export { RootModel };
export type Root = Instance<typeof RootModel>;
export type Test = Instance<typeof TestModel>;
React component for Photos.tsx
import React, { Component } from 'react';
import Spinner from 'components/Spinner';
import { Root } from '../../stores';
import { observer, inject } from 'mobx-react';
interface Props {
rootTree?: Root
}
#inject('rootTree')
#observer
class Photos extends Component<Props> {
componentDidMount() {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.data.fetchData('photo');
}
componentDidUpdate(prevProps) {
if (prevProps.ctx !== this.props.ctx) {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.data.fetchData('photo');
}
}
displayPhoto() {
const { rootTree } = this.props;
if (!rootTree) return null;
// calling method in MST view
const photoResults = rootTree.data.getPhoto();
if (photoResults.$treenode.snapshot[0]) {
return (
<div>
<div className='photo-title'>{'Photo'}</div>
{photoResults.$treenode.snapshot.map(Item => (
<a href={photoItem.attributes.openUrl} target='_blank'>
<img src={photoItem.url} />
</a>
))}
</div>
);
} else {
return <Spinner />;
}
}
render() {
return <div className='photo-module'>{this.displayPhoto()}</div>;
}
}
export default Photos;
Similarly, Tutorials.tsx is like so:
import React, { Component } from 'react';
import Spinner from '';
import { Root } from '../../stores';
import { observer, inject } from 'mobx-react';
interface Props {
rootTree?: Root;
}
#inject('rootTree')
#observer
class Tutorials extends Component<Props> {
componentDidMount() {
if (this.props.ctx) {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.data.fetchData('tuts');
}
}
componentDidUpdate(prevProps) {
if (prevProps.ctx !== this.props.ctx) {
const { rootTree } = this.props;
if (!rootTree) return null;
rootTree.search.fetchData('tuts');
}
}
displayTutorials() {
const { rootTree } = this.props;
if (!rootTree) return null;
// calling method in MST view
const tutResults = rootTree.data.getTutorials();
if (tutResults.$treenode.snapshot[0]) {
return (
<div>
<div className='tutorials-title'>{'Tutorials'}</div>
{tutResults.$treenode.snapshot.map(tutorialItem => (
<a href={tutorialItem.attributes.openUrl} target='_blank'>
<img src={tutorialItem.url} />
</a>
))}
</div>
);
} else {
return <Spinner />;
}
}
render() {
return <div className='tutorials-module'>{this.displayTutorials()}</div>;
}
}
export default Tutorials;
Why are you using applySnapshot at all in this case? I don't think it's necessary. Just assign your data as needed in your action:
.actions(self => ({
//If you're fetching both at the same time
fetchData: flow(function* fetchData(param) {
const results = yield api.fetch(param);
//you need cast() if using Typescript otherwise I think it's optional
self.photos = cast([...results.photos, ...self.photos])
//do you really intend to prepend the results to the existing array or do you want to overwrite it with the sever response?
self.tutorials = cast(results.tutorials)
})
}))
Or if you need to make two separate requests to fetch your data it's probably best to make it two different actions
.actions(self => ({
fetchPhotos: flow(function* fetchPhotos(param) {
const results = yield api.fetch(param)
self.photos = cast([... results, ...self.photos])
}),
fetchTutorials: flow(function* fetchTutorials(param) {
const results = yield api.fetch(param)
self.tutorials = cast([... results, ...self.tutorials])
}),
}))
Regardless, it doesn't seem like you need applySnapshot. Just assign your data in your actions as necessary. There's nothing special about assigning data in an async action.
import React, { Component } from 'react';
import {withProvider} from './TProvider'
import ThreeCardMap from './ThreeCardMap';
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
componentDidMount(){
this.props.getList()
this.setState({newlist: [this.props.list]})
}
// componentDidUpdate() {
// console.log(this.state.newlist);
// }
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);
Hi, I'm trying to create a page that takes data from a tarot card API (https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major). Unfortunately by the time the data comes in, my map function has already fired. I'm asking to see if there is a way to have the map function wait until the data hits before it fires. Thanks!
Edit: getList function in the Context:
getList = () => {
console.log('fired')
axios.get('https://vschool-cors.herokuapp.com?url=https://rws-cards-api.herokuapp.com/api/v1/cards/search?type=major').then(response =>{
this.setState({
list: response.data
})
}).catch(error => {
console.log(error);
})
}
this.props.getList() is an async function. You are setting the list right after that call which is not correct.
You need to set it in the getList promise then() block.
getList() is an async function and update data for the parent component. So, my solution is just watching the list from the parent component if they updated or not, through getDerivedStateFromProps
class Threecard extends Component {
constructor() {
super();
this.state = {
newlist: []
}
}
// Set props.list to this.state.newList and watch the change to update
static getDerivedStateFromProps(nextProps, prevState) {
return {
newlist: nextProps.list
}
}
componentDidMount(){
this.props.getList()
// Removed this.setState() from here.
}
render() {
const MappedTarot = (this.state.newlist.map((list, i) => <ThreeCardMap key={i} name={list.name} meaningup={list.meaning_up} meaningdown={list.meaning_rev}/>);
return (
<div>
<h1>Three Card Reading</h1>
<div>{ MappedTarot }</div>
</div>
)
}
}
export default withProvider(Threecard);
I have the following TypeScript code. I simplified/remove as much as I could.
interface DataPullingPageState
{
loading: boolean;
displayedEntries: string[];
};
export class EntriesPageOne extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const entries = await api.loadAll();
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
export class EntriesPageTwo extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const param = "my param";
const entries = await api.loadByStringParam(param);
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
export class EntriesPageThree extends React.Component<{}, DataPullingPageState>
{
constructor(props: any)
{
super(props);
this.state = { loading: false, displayedEntries: [] };
}
async componentDidMount()
{
this.setState({ loading: true });
const param = 123;
const entries = await api.loadByNumberParam(param);
this.setState({ loading: false, displayedEntries: entries });
}
render()
{
if (this.state.loading)
{
return <div>loading</div>;
}
else if (this.state.displayedEntries.length === 0)
{
return <div>nothing found</div>;
}
else
{
return this.state.displayedEntries.map((entry, i) => <div key={i}>{entry}</div>);
}
}
}
As you can see it's three different components that all display the same but have three different ways of loading it.
I'd like to know how I can make only one component out of those three. I've already heard about HoC but don't know if they suit my case.
Yes you can HoC let's simplify your code a bit:
HoC Method
class EntriesPage extends React.Component {
// you don't need state for loading
render() {
const { loading, entries } = this.props
}
}
EntriesPage.defaultProps = { loading: true, entries: [] }
const withEntries = (apiCall) => (Page) => {
return async (props) => {
const entries = await apiCall()
<Page {...props} loading={false} entries={entries} />
}
}
Now you can compose first page like this
// PageOne
export default withEntries(api.loadAll)(EntriesPage)
// PageTwo
export default withEntries(() => api.loadByStringParam('param'))(EntriesPage)
// PageThree
export default withEntries(() => api.loadByNumberParam(123))(EntriesPage)
This will create HoC which accepts dynamic fetching method and pass the result as prop to the final component. Hope this helps
Hoc method with param as prop
You can even expose params to the final component by changing it to something like this
const withEntries = (apiCall) => (Page) => {
return async (props) => {
const { fetchParam, ...rest } = props
const entries = await apiCall(fetchParam)
<Page {...rest} loading={false} entries={entries} />
}
}
// EntriesPageComposed.tsx
export default withEntries(api.loadByStringParam)(EntriesPage)
<EntriesPageComposed fetchParams={123} />
Loader component
Or you can even make it completely isolated without HoC and pass everything as prop and make "data loader" component, which is quite common pattern in React apps, which will act only as loader for preparing next props.
const ComposedComponent = async (props) => {
const { fetchMethod, fetchParam, ...rest } = props
const entries = await fetchMethod(fetchParam)
return (
<EntriesPage {...rest} loading={false} entries={entries} />
)
}
<ComposedComponent fetchMethod={api.loadByStringParam} fetchParam={'param'} />
In this way you have initial implementation isolated and you can add new fetch methods on the fly just by passing a prop.
I have this code:
export default class MainStudentPage extends React.Component {
constructor(props) {
super(props);
this.state = {user: {nickname: '', friends: {accepted: [], invites: [], all: []}}};
}
componentWillMount() {
const {uid} = firebase.auth().currentUser;
firebase.database().ref('Users').child(uid).on('value', (r, e) => {
if (e) {
console.log(e);
return null;
}
const user = r.val();
this.setState({user: user});
});
}
render() {
const {user} = this.state;
return (
<LevelSelectComponent user={user}/>
</div>
);
}
}
And this is the child:
export default class LevelSelectComponent extends React.Component {
returnSelect = (user) => {
const lvls = [{
db: 'Podstawówka',
text: 'PODSTAWOWA'
}, {
db: 'Gimnazjum',
text: 'GIMNAZJALNA'
}, {
db: 'Liceum',
text: 'ŚREDNIA'
}];
let options = [];
if (!user.level) {
options.push(<option selected={true} value={null}>WYBIERZ POZIOM</option>)
}
options = options.concat(lvls.map((lvl, i) => {
return (
<option key={i} value={lvl.db}>{`SZKOŁA ${lvl.text}`}</option>
)
}));
return (
<select defaultValue={user.level}>
{options.map(opt => opt)}
</select>
)
};
render() {
const {user} = this.props;
return (
this.returnSelect(user)
);
}
}
So what I want is to refresh the default selected value to match the value in the database. I am listening to the firebase realtime database for changes. Every time I refresh the page, the defaultValue changes, as expected, but this doesn't do it in real time. It even logs the new value, but it doesn't rerender it. What am I missing?
Ok. All I had to do was change defaultValue to value
componentWillMount() this should not be the method where you use AJAX requests
instead, user componentDidMount().
Further:
componentWillMount() will only be invoked once, before the first render() for your component, thus it will not trigger a re-render for it, you should subscribe to your firebase realtime events in componentDidMount().