Update Rendered Component Prop Value Async - reactjs

I creating a couple of lists items with financial data and I want to display a mini graph on the side using react-sparklines.
So I'm trying to fetch the graph data when mapping the array since retrieving the data could take some time but it seems I can't get the graph component to update the prop value correctly.
How can I update the prop data in the Sparklines component once the data has been retreived from fetch?, is it possible?.
Here's a sample of my code
class CurrencyList extends Component {
currencyGraph(symbol) {
return fetch(baseURL, {
method: 'POST',
headers: {
'Authorization': 'JWT ' + token || undefined, // will throw an error if no login
},
body: JSON.stringify({'symbol': symbol})
})
.then(handleApiErrors)
.then(response => response.json())
.then(function(res){
if (res.status === "error") {
var error = new Error(res.message)
error.response = res.message
throw error
}
return res.graph_data
})
.catch((error) => {throw error})
}
render () {
topCurrencies.map((currency,i) => {
return (
<ListItem key={i} button>
<Sparklines data={this.currencyGraph(currency.symbol)} >
<SparklinesLine style={{ stroke: "white", fill: "none" }} />
</Sparklines>
<ListItemText primary={currency.symbol} />
</ListItem>
)
})
}
}

I would wrap this component with my own component and accept the data as props.
In the parent component i will render the list only when i have the data ready and pass each component the relevant data on each iteration.
Here is a running small example
const list = [
{
key: 1,
data: [5, 10, 5, 20]
},
{
key: 2,
data: [15, 20, 5, 50]
},
{
key: 3,
data: [1, 3, 5, 8]
}
];
class MySparklines extends React.Component {
render() {
const { data } = this.props;
return (
<Sparklines data={data} limit={5} height={20}>
<SparklinesLine style={{ fill: "#41c3f9" }} />
</Sparklines>
);
}
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
dataList: []
}
}
componentDidMount() {
setTimeout(() => {
this.setState({ dataList: list });
}, 1500);
}
renderCharts = () => {
const { dataList } = this.state;
return dataList.map((d) => {
return(
<MySparklines key={d.key} data={d.data} />
);
})
}
render() {
const { dataList } = this.state;
return (
<div>
{
dataList.length > 0 ?
this.renderCharts() :
<div>Loading...</div>
}
</div>
);
}
}

You can't return data from an asynchronous function like that. Instead, when your load request completes, set the data in the state which will trigger the component to render. Note that in the test case below, it renders a placeholder until the data is ready.
Item = props => <div>{props.name}</div>;
class Test extends React.Component {
state = {
data: ''
}
componentDidMount() {
// Simulate a remote call
setTimeout(() => {
this.setState({
data: 'Hi there'
});
}, 1000);
}
render() {
const { data } = this.state;
return data ? <Item name={this.state.data} /> : <div>Not ready yet</div>;
}
}
ReactDOM.render(
<Test />,
document.getElementById('container')
);
Fiddle

Related

Reactjs: How to properly fetch each users record from database on pop button click using Reactjs

The code below shows each user info on users list button click.
Now I want fetch each users record from database on users list button click.
In the open() function, I have implemented the code below
open = (id,name) => {
alert(id);
alert(name);
//start axios api call
const user_data = {
uid: 'id',
uname: 'name'
};
this.setState({ loading_image: true }, () => {
axios.post("http://localhost/data.php", { user_data })
.then(response => {
this.setState({
chatData1: response.data[0].id,
chatData: response.data,
loading_image: false
});
console.log(this.state.chatData);
alert(this.state.chatData1);
})
.catch(error => {
console.log(error);
});
});
}
In class OpenedUser(), I have initialize in the constructor the code below
chatData: []
In the render method have implemented the code
<b> Load Message from Database for each user ({this.state.chatData1})</b>
<div>
{this.state.chatData.map((pere, i) => (<li key={i}>{pere.lastname} - {pere.id}----- {pere.username}</li>))}
</div>
Here is my Issue:
My problem is that the Axios Api is getting the result but am not seeing any result in the render method.
but I can see it in the console as per code below
Array(1)
0: {id: "1", firstname: "faco", lastname: "facoyo"}
length: 1
Here is an example of json api response.
[{"id":"1","firstname":"faco","lastname":"facoyo"}]
Here is the full code
import React, { Component, Fragment } from "react";
import { render } from "react-dom";
import { Link } from 'react-router-dom';
import axios from 'axios';
class User extends React.Component {
open = () => this.props.open(this.props.data.id, this.props.data.name);
render() {
return (
<React.Fragment>
<div key={this.props.data.id}>
<button onClick={() => this.open(this.props.data.id,this.props.data.name)}>{this.props.data.name}</button>
</div>
</React.Fragment>
);
}
}
class OpenedUser extends React.Component {
constructor(props) {
super(props);
this.state = {
chatData: [],
hidden: false,
};
}
componentDidMount(){
} // close component didmount
toggleHidden = () =>
this.setState(prevState => ({ hidden: !prevState.hidden }));
close = () => this.props.close(this.props.data.id);
render() {
return (
<div key={this.props.data.id} style={{ display: "inline-block" }}>
<div className="msg_head">
<button onClick={this.close}>close</button>
<div>user {this.props.data.id}</div>
<div>name {this.props.data.name}</div>
{this.state.hidden ? null : (
<div className="msg_wrap">
<div className="msg_body">Message will appear here</div>
<b> Load Message from Database for each user ({this.state.chatData1}) </b>
<div>
{this.state.chatData.map((pere, i) => (
<li key={i}>
{pere.lastname} - {pere.id}----- {pere.username}
</li>
))}
</div>
</div>
)}
</div>
</div>
);
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
shown: true,
activeIds: [],
data: [
{ id: 1, name: "user 1" },
{ id: 2, name: "user 2" },
{ id: 3, name: "user 3" },
{ id: 4, name: "user 4" },
{ id: 5, name: "user 5" }
],
};
}
toggle() {
this.setState({
shown: !this.state.shown
});
}
open = (id,name) => {
alert(id);
alert(name);
//start axios api call
const user_data = {
uid: 'id',
uname: 'name'
};
this.setState({ loading_image: true }, () => {
axios.post("http://localhost/apidb_react/search_data.php", { user_data })
.then(response => {
this.setState({
chatData1: response.data[0].id,
chatData: response.data,
loading_image: false
});
console.log(this.state.chatData);
alert(this.state.chatData1);
})
.catch(error => {
console.log(error);
});
});
// end axios api call
this.setState((prevState) => ({
activeIds: prevState.activeIds.find((user) => user === id)
? prevState.activeIds
: [...prevState.activeIds, id]
}));
}
close = id => {
this.setState((prevState) => ({
activeIds: prevState.activeIds.filter((user) => user !== id),
}));
};
renderUser = (id) => {
const user = this.state.data.find((user) => user.id === id);
if (!user) {
return null;
}
return (
<OpenedUser key={user.id} data={user} close={this.close}/>
)
}
renderActiveUser = () => {
return (
<div style={{ position: "fixed", bottom: 0, right: 0 }}>
{this.state.activeIds.map((id) => this.renderUser(id)) }
</div>
);
};
render() {
return (
<div>
{this.state.data.map(person => (
<User key={person.id} data={person} open={this.open} />
))}
{this.state.activeIds.length !== 0 && this.renderActiveUser()}
</div>
);
}
}
The problem is you're making the request in the App component and storing in state but you're trying to access the state in a child component so it will never actually read the data.
To fix this you need to pass in the chat data via prop
<OpenedUser
chatData={this.state.chatData}
key={user.id}
data={user}
close={this.close}
/>
Note: In my runnable example, I've replaced your api endpoint with a mock api promise.
const mockApi = () => {
return new Promise((resolve, reject) => {
const json = [{ id: "1", firstname: "faco", lastname: "facoyo" }];
resolve(json);
});
};
class User extends React.Component {
open = () => this.props.open(this.props.data.id, this.props.data.name);
render() {
return (
<React.Fragment>
<div key={this.props.data.id}>
<button
onClick={() => this.open(this.props.data.id, this.props.data.name)}
>
{this.props.data.name}
</button>
</div>
</React.Fragment>
);
}
}
class OpenedUser extends React.Component {
constructor(props) {
super(props);
this.state = {
hidden: false
};
}
componentDidMount() {} // close component didmount
toggleHidden = () =>
this.setState(prevState => ({ hidden: !prevState.hidden }));
close = () => this.props.close(this.props.data.id);
render() {
return (
<div key={this.props.data.id} style={{ display: "inline-block" }}>
<div className="msg_head">
<button onClick={this.close}>close</button>
<div>user {this.props.data.id}</div>
<div>name {this.props.data.name}</div>
{this.state.hidden ? null : (
<div className="msg_wrap">
<div className="msg_body">Message will appear here</div>
<b>
{" "}
Load Message from Database for each user ({this.state.chatData1}
){" "}
</b>
<ul>
{this.props.chatData.map((pere, i) => (
<li key={i}>
{pere.lastname} - {pere.id}----- {pere.username}
</li>
))}
</ul>
</div>
)}
</div>
</div>
);
}
}
class App extends React.Component {
constructor() {
super();
this.state = {
shown: true,
chatData: [],
activeIds: [],
data: [
{ id: 1, name: "user 1" },
{ id: 2, name: "user 2" },
{ id: 3, name: "user 3" },
{ id: 4, name: "user 4" },
{ id: 5, name: "user 5" }
]
};
}
toggle() {
this.setState({
shown: !this.state.shown
});
}
open = (id, name) => {
alert(id);
alert(name);
//start axios api call
const user_data = {
uid: "id",
uname: "name"
};
// this.setState({ loading_image: true }, () => {
// axios
// .post("http://localhost/apidb_react/search_data.php", { user_data })
// .then(response => {
// this.setState({
// chatData1: response.data[0].id,
// chatData: response.data,
// loading_image: false
// });
// console.log(this.state.chatData);
// alert(this.state.chatData1);
// })
// .catch(error => {
// console.log(error);
// });
// });
this.setState({ loading_image: true }, () => {
mockApi().then(data => {
this.setState({
chatData1: data[0].id,
chatData: data,
loading_image: false
});
});
});
// end axios api call
this.setState(prevState => ({
activeIds: prevState.activeIds.find(user => user === id)
? prevState.activeIds
: [...prevState.activeIds, id]
}));
};
close = id => {
this.setState(prevState => ({
activeIds: prevState.activeIds.filter(user => user !== id)
}));
};
renderUser = id => {
const user = this.state.data.find(user => user.id === id);
if (!user) {
return null;
}
return (
<OpenedUser
chatData={this.state.chatData}
key={user.id}
data={user}
close={this.close}
/>
);
};
renderActiveUser = () => {
return (
<div style={{ position: "fixed", bottom: 0, right: 0 }}>
{this.state.activeIds.map(id => this.renderUser(id))}
</div>
);
};
render() {
return (
<div>
{this.state.data.map(person => (
<User key={person.id} data={person} open={this.open} />
))}
{this.state.activeIds.length !== 0 && this.renderActiveUser()}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<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>
<div id="root"></div>
I see a few missing points in your code namely you are using li without ul which is a kind of invalid markup, then you have mapping for .username which is undefined field according to response which may also throw error.

Reactjs Search after onChange

I have implemented a little search functionality in my reactjs application.
The Problem is, that my "searchHandler" function is triggered after every single letter the user enters in the textfield... So e.g. for the term "Lorem" my function is fetching 5 times from my api :(
How can I solve this problem?
Here is my code:
const { scaleDown } = transitions;
function searchingFor(term){
return function(x){
return x.title.toLowerCase().includes(term.toLowerCase()) ||
x.body.toLowerCase().includes(term.toLowerCase());
}
}
class ViewAll extends React.Component{
constructor(props){
super(props);
this.state = {
term: '',
mounted: true,
tracks: [],
hasMoreItems: true,
page: 2,
}
this.searchHandler = this.searchHandler.bind(this);
this.focus = this.focus.bind(this);
this.keyPress = this.keyPress.bind(this);
}
loadContent() {
var requestUrl = this.props.url;
fetch(requestUrl + this.state.page + '&_limit=3').then((response)=>{
return response.json();
}) .then((tracks)=>{
this.setState({ tracks: this.state.tracks.concat(tracks)});
this.setState({page: this.state.page + 1});
if(this.state.page === 6){
this.setState({hasMoreItems: false})
}
}).catch((err)=>{
console.log("There has been an error");
});
}
componentDidMount() {
window.scrollTo(0, 0);
var requestUrl = this.props.url;
fetch(requestUrl + '1&_limit=3')
.then((response)=>{
return response.json();
}) .then((data)=>{
this.setState({tracks : data});
})
.catch((err)=>{
console.log("There has been an error");
});
//this.focus();
}
searchHandler(event){
this.setState({term: event.target.value});
var requestUrl = 'https://questdb.herokuapp.com/all?q='
fetch(requestUrl + this.state.term).then((response)=>{
return response.json();
}) .then((tracks)=>{
this.setState({ tracks: this.state.tracks.concat(tracks)});
}).catch((err)=>{
console.log("There has been an error");
});
}
focus() {
this.textInput.focus();
}
keyPress(e){
if(e.keyCode == 13){
console.log('value', e.target.value);
// put the login here
}
}
render() {
const {term, data, tracks} = this.state;
const loader = <div className="loader2"> </div>;
var items = [];
const imageUrl = require(`../assets/Book.jpg`)
tracks.filter(searchingFor(term)).map(function(title, i)
{
items.push(
<div>
<MuiThemeProvider>
<Paper style={{ borderRadius: "2em",
background: '#ffffff'
}} zDepth={1} >
<ItemViewAll
key={title.id}
/>
</Paper>
</MuiThemeProvider>
</div>
);
}, this);
return (
<div>
<Fade in={true} timeout={1000}>
<div >
<MuiThemeProvider>
<TextField hintText='Bot suchen...'
type="Text"
onChange={this.searchHandler}
value={term}
underlineFocusStyle={{borderColor: '#B00020', borderWidth: 3}}
underlineStyle={{borderColor: '#B00020', borderWidth: 1.5, top: '45px'}}
hintStyle={{fontSize: '8.1vw', fontFamily: 'Anton', color: 'rgba(255,255,255,0.9)'}}
inputStyle={{fontSize: '8.1vw', fontFamily: 'Anton', color: '#ffffff'}}
ref={(input) => { this.textInput = input; }}
style={{caretColor: '#ffffff', width: '90%', maginLeft: 'auto', marginRight: 'auto', marginTop: '12%' }}
InputLabelProps={{ shrink: true }}
/>
</MuiThemeProvider>
</div>
</Fade>
<InfiniteScroll
pageStart={1}
loadMore={this.loadContent.bind(this)}
hasMore={this.state.hasMoreItems}
initialLoad={true}
>
{items}
</InfiniteScroll>
</div>
)
}
}
export default ViewAll;
Here you can check out the Website with the broken search function. As you can see the items are shown double or even triple... After the textfield is emptied, the search results should be removed and only the normal fetched ones should be shown.
https://www.genko.de (use the mobile version in chrome)
Thank you :)
Use lodash debounce. It is used for this exact use case
https://stackoverflow.com/questions/48046061/using-lodash-debounce-in-react-to-prevent-requesting-data-as-long-as-the-user-is
Sample:
import React, {Component} from 'react'
import { debounce } from 'lodash'
class TableSearch extends Component {
//********************************************/
constructor(props){
super(props)
this.state = {
value: props.value
}
this.changeSearch = debounce(this.props.changeSearch, 250)
}
//********************************************/
handleChange = (e) => {
const val = e.target.value
this.setState({ value: val }, () => {
this.changeSearch(val)
})
}
//********************************************/
render() {
return (
<input
onChange = {this.handleChange}
value = {this.props.value}
/>
)
}
//********************************************/
}
If you don't need full lodash package you can write it yourself:
function debounce(f, ms) {
let timer = null;
return function (...args) {
const onComplete = () => {
f.apply(this, args);
timer = null;
}
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(onComplete, ms);
};
}
The first arg (f) is your function which should not be performed more often than
second arg (ms) - amount of ms ). So in your case you can write your handler in next way:
handleChange = debounce((e) => {
const val = e.target.value
this.setState({ value: val }, () => {
this.changeSearch(val)
})
}, 1000) // second arg (1000) is your amount of ms

Duplicate asynchronous component twice render the last

I create the same component (Recipes) or Screen twice, sending different properties.
<Recipes setRecipes = {this.setRecipes} id="1" />
<Quantity setQuantity = {this.setQuantity} />
<Recipes setRecipes = {this.setRecipes2} id="2" />
<Quantity setQuantity = {this.setQuantity} />
The properties-functions are the following. Only states change.
setRecipes = (recipe) => {
this.setState({recipe:recipe})
}
setRecipes2 = (recipe2) => {
this.setState({recipe2:recipe2})
}
In mty component called Recipes, I get my local database (I use pouchdb), my recipes and products asicronically.
import React, { Component } from 'react';
import { Text, View, TouchableOpacity, ScrollView } from 'react-native';
import style from './Styles/styleNew';
import PouchDB from 'pouchdb-react-native';
PouchDB.plugin(require('pouchdb-find'));
const gThis = null;
const db = new PouchDB('X')
export default class Recipes extends Component {
constructor(props) {
super(props);
gThis = this;
this.state = {
setRecipes: this.props.setRecipes,
id: this.props.id,
recipeID: null,
options: [ ],
};
}
getRecipes() {
let data = []
db.find({
selector: {
type: {
"$eq": this.state.id,
},
owner: {
"$in": ['user']
},
},
}).then(function (recipes) {
recipes = recipes.docs
for (let i = 0; i < recipes.length; i++) {
recipe = recipes[i]
let current = {
id: recipe._id,
name: recipe.name,
recipe: recipe,
}
data.push(current)
}
gThis.setState({ options: data })
}).catch(function (err) {
console.warn(err.toString())
});
}
componentDidMount() {
this.getRecipes()
}
handleBackPress = () => {
this.props.navigation.goBack();
}
defineRecipe(recipe, index) {
this.setState({ recipeID: recipe._id });
this.state.setRecipes(recipe)
}
render() {
return (
<View style={{ flex: 1 }}>
<Text style={style.listText}>RECIPES {this.state.id} </Text>
<ScrollView style={style.listScroll}>
{this.state.options.length >0 &&
this.state.options.map((item, index) => {
key = index + '-' + this.state.id
//key = new Date().toISOString()
console.log(key)
return (
<TouchableOpacity
style={this.state.recipeID === item.id ? style.listOptionSelected : style.listOption}
onPress={() => { this.defineRecipe(item.recipe, index);}}
key={key} >
<Text style={style.listOptionText}>{item.name}</Text>
</TouchableOpacity>
);
})
}
</ScrollView>
</View>
);
}
}
But the second Screen Recipe is rendered twice.
Render 1
Render 2
As you can see, the content is replaced.
I think it might have something to do with you gThis which I assume is a global this?
A better way to try and access this inside your class members is to either use lexical arrow functions in your class like this:
getRecipes = () => {
// implementation
}
Or bind your class members to this in the constructor as follows
constructor(props) {
super(props);
gThis = this;
this.state = {
id: this.props.id,
recipeID: null,
options: [ ],
};
this.getRecipes = this.getRecipes.bind(this);
this.defineRecipe = this.defineRecipe.bind(this);
}
On a different node, you have babel transpiling your ES6 code from as far as I can see.
I would strongly recommend replacing your for loops with es6 variants such as a map or forEach for readability
const options = recipes.map(recipe => {
const { _id: id, name } = recipe;
data.push({
id,
name,
recipe,
});
});
this.setState({ options });

How to generate snapshot after all life cycle methods have called in React Jest

snapshot file has created before componentDidMount() is being called. In my situation, I fetch data from server inside the componentDidMount(). Based on the results, I draw the table. But in my test case, it doesn't show those received mock results.
Test file
import React from 'react';
import renderer from 'react-test-renderer';
import { fakeRequestLibrary } from '../../../__mocks__/fakeRequestLibrary';
import ReportAsTableView from '../../../components/reports/common/ReportAsTableView';
const FAKE_RESPONSE = {
dataSets: [
{
metadata: {
columns: [
{
name: "username",
label: "username"
},
{
name: "date_created",
label: "date_created"
}
]
},
rows: [
{
date_created: "2010-04-26T13:25:00.000+0530",
username: "daemon"
},
{
date_created: "2017-06-08T21:37:18.000+0530",
username: "clerk"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "nurse"
},
{
date_created: "2017-07-08T21:37:19.000+0530",
username: "doctor"
},
{
date_created: "2017-07-08T21:37:18.000+0530",
username: "sysadmin"
}
]
}
]
};
describe('<ReportAsTableView /> ', () => {
it('renders correctly with success data received from server', () => {
const params = {
"startDate": "2017-05-05",
"endDate": "2017-10-05"
};
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary('openmrs-fake-server.org', {}, true, FAKE_RESPONSE)} />
);
expect(rendered.toJSON()).toMatchSnapshot();
});
});
Targeted component class
import React, { Component } from 'react';
import { ApiHelper } from '../../../helpers/apiHelper';
import * as ReportConstants from '../../../helpers/ReportConstants';
import ReactDataGrid from 'react-data-grid';
import DataNotFound from './DataNotFound';
import moment from 'moment';
import './ReportAsTableView.css';
class ReportAsTableView extends Component {
constructor(props) {
super();
this.state = {
report: {
definition: {
name: ''
}
},
reportColumnNames: Array(),
reportRowData: Array()
};
this.resolveResponse = this.resolveResponse.bind(this);
this.rowGetter = this.rowGetter.bind(this);
this.init = this.init.bind(this);
}
componentDidMount() {
this.init(this.props.reportParameters);
}
componentWillReceiveProps(nextProps) {
this.init(nextProps.reportParameters);
}
init(params) {
if(this.props.fetchData != null){
//Test Path
this.props.fetchData
.then((response) => {
console.log('>>>>>'+JSON.stringify(response.body));
this.resolveResponse(response.body);
});
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
resolveResponse(data) {
this.setState({ report: data });
this.setState({ reportColumnNames: data.dataSets[0].metadata.columns });
this.setState({ reportRowData: data.dataSets[0].rows });
}
// ... there are some other methods as well
render() {
return (
<div style={{ border: '1px solid black' }}>
{this.getColumns().length > 0 ? (
<ReactDataGrid
columns={this.getColumns()}
rowGetter={this.rowGetter}
rowsCount={this.state.reportRowData.length} />
) : (
<DataNotFound componentName="Report Table"/>
)}
</div>
);
}
}
export default ReportAsTableView;
Snapshot file
// Jest Snapshot v1,
exports[`<ReportAsTableView /> renders correctly with success data received from server 1`] = `
<div
style={
Object {
"border": "1px solid black",
}
}
>
<div
className="NotFoundWrapper"
>
<div
className="attentionSign"
>
<img
src="./warning.png"
width="300"
/>
</div>
<div>
No Data found
<span>
for
Report Table
</span>
</div>
</div>
</div>
`;
Update:
fakeRequestLibrary
import Response from 'http-response-object';
export const fakeRequestLibrary = (requestUrl, requestOptions, shouldPass = true, responseData = null) => {
return new Promise((resolve, reject) => {
if (shouldPass) {
resolve(new Response(200, {}, responseData || { message: `You called ${requestUrl}` }, requestUrl));
} else {
reject(new Response(404, {}, responseData || { message: `The page at ${requestUrl} was not found` }, requestUrl));
}
});
};
Instead of passing an http end point what you can do for fix your problem is changing your init method and passing the data if no data are passed fetch them. Like this
init(params) {
if(this.props.fetchData != null){
this.resolveResponse(this.props.fetchData);
}else{
new ApiHelper().post(ReportConstants.REPORT_REQUEST + this.props.reportUUID, params)
.then((response) => {
this.resolveResponse(response);
});
}
}
Then in your test you will have
var rendered = renderer.create(
<ReportAsTableView reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={FAKE_RESPONSE} />
);
expect(rendered.toJSON()).toMatchSnapshot();
This solution works for my own project. It might also work for this question as well, but I haven't tested it. Add an await wait(); statement to wait for the async function in componentDidMount to complete.
const wait = async () => 'foo'; // a dummy function to simulate waiting
describe('<ReportAsTableView /> ', async () => {
it('renders correctly with success data received from server', async () => {
const params = {
startDate: '2017-05-05',
endDate: '2017-10-05',
};
var rendered = renderer.create(
<ReportAsTableView
reportUUID="e451ae04-4881-11e7-a919-92ebcb67fe33"
reportParameters={params}
fetchData={fakeRequestLibrary(
'openmrs-fake-server.org',
{},
true,
FAKE_RESPONSE,
)}
/>,
);
await wait(); // wait for <ReportAsTableView> to finish async data fetch in componentDidMount()
expect(rendered.toJSON()).toMatchSnapshot(); // shall render the component AFTER componentDidMount() is called
});
});

React native delete multiple items from state array

I have a directory which stores images taken using the camera. For saving images I am using RNFS. I am using react-native-photo-browser.
The gallery itself doesn't have any options to delete the items from the gallery. So I am working to achieve it
export default class GridGallery extends React.Component{
static navigationOptions = {
title: 'Image Gallery',
};
constructor(props) {
super(props)
this.state = {
filesList : [],
mediaSelected: [],
base64URI: null,
galleryList: []
}
}
componentDidMount(){
FileList.list((files) => {
if(files != null) {
this.fileUrl = files[0].path;
files = files.sort((a, b) => {
if (a.ctime < b.ctime)
return 1;
if (a.ctime > b.ctime)
return -1;
return 0;
});
this.setState({
filesList: files
});
}
console.warn(this.state.filesList);
this.getFiles();
});
}
getFiles(){
//console.warn(this.state.filesList);
const ArrFiles = this.state.filesList.map((file) =>
({ caption : file.name, photo : file.path })
);
//console.warn(ArrFiles);
this.setState({ galleryList : ArrFiles });
}
onActionButton = (media, index) => {
if (Platform.OS === 'ios') {
ActionSheetIOS.showShareActionSheetWithOptions(
{
url: media.photo,
message: media.caption,
},
() => {},
() => {},
);
} else {
alert(`handle sharing on android for ${media.photo}, index: ${index}`);
}
};
handleSelection = async (media, index, isSelected) => {
if (isSelected == true) {
this.state.mediaSelected.push(media.photo);
} else {
this.state.mediaSelected.splice(this.state.mediaSelected.indexOf(media.photo), 1);
}
console.warn(this.state.mediaSelected);
}
deleteImageFile = () => {
const dirPicutures = RNFS.DocumentDirectoryPath;
//delete mulitple files
console.warn(this.state.mediaSelected);
this.state.mediaSelected.map((file) =>
// filepath = `${dirPicutures}/${file}`
RNFS.exists(`${file}`)
.then( (result) => {
console.warn("file exists: ", result);
if(result){
return RNFS.unlink(`${file}`)
.then(() => {
console.warn('FILE DELETED');
let tempgalleryList = this.state.galleryList.filter(item => item.photo !== file);
this.setState({ galleryList : tempgalleryList })
})
// `unlink` will throw an error, if the item to unlink does not exist
.catch((err) => {
console.warn(err.message);
});
}
})
.catch((err) => {
console.warn(err.message);
})
)
}
renderDelete(){
const { galleryList } = this.state;
if(galleryList.length>0){
return(
<View style={styles.topRightContainer}>
<TouchableOpacity style={{alignItems: 'center',right: 10}} onPress={this.deleteImageFile}>
<Image
style={{width: 24, height: 24}}
source={require('../assets/images/ic_delete.png')}
/>
</TouchableOpacity>
</View>
)
}
}
goBack() {
const { navigation } = this.props;
navigation.pop;
}
render() {
const { galleryList } = this.state;
return (
<View style={styles.container}>
<View style={{flex: 1}}>
<PhotoBrowser
mediaList={galleryList}
enableGrid={true}
displayNavArrows={true}
displaySelectionButtons={true}
displayActionButton={true}
onActionButton={this.onActionButton}
displayTopBar = {true}
onSelectionChanged={this.handleSelection}
startOnGrid={true}
initialIndex={0}
/>
</View>
{this.renderDelete()}
</View>
)
}
}
An example list of images:
[
{
photo:'4072710001_f36316ddc7_b.jpg',
caption: 'Grotto of the Madonna',
},
{
photo: /media/broadchurch_thumbnail.png,
caption: 'Broadchurch Scene',
},
{
photo:
'4052876281_6e068ac860_b.jpg',
caption: 'Beautiful Eyes',
},
]
My aim is whenever the item from state galleryList is removed I need to refresh the component, so the deleted image will be removed from the gallery. So When I try to use filter the galleryList it deleting other images instead of other images:
let tempgalleryList = this.state.galleryList.filter(item => item.photo !== file);
this.setState({ galleryList : tempgalleryList })
MCVE -> This is a minified version of my code, you can see the images are deleting randomly
Problem
let tempgalleryList = this.state.galleryList.filter(item => item.photo !== file);
this.setState({ galleryList : tempgalleryList })
Since setState is async, this.state.galleryList will not be updated in each iteration of your map function, so the final updated state will only have one item filtered out instead of all selected items.
Solution
You can use the callback version of setState which uses the updated state instead:
this.setState(prevState => ({
galleryList : prevState.galleryList.filter(item => item.photo !== file),
}));
Alternative solution
Instead of calling setState in every iteration, you can call it outside of your map function instead (though setState updates will be batched anyway so no significant performance improvement):
this.setState(prevState => ({
galleryList : prevState.galleryList.filter(item => !prevState.mediaSelected.includes(item.photo)),
}));
Other problems with your code
this.state.mediaSelected.push(media.photo);
} else {
this.state.mediaSelected.splice(this.state.mediaSelected.indexOf(media.photo), 1);
You are directly mutating your state here. Do this instead:
this.setState(prevState => ({
mediaSelected: prevState.mediaSelected.concat(media.photo)
}));
this.setState(prevState => ({
mediaSelected: prevState.mediaSelected.filter(e => e != media.photo)
}));

Resources