I am trying to create a simple table using ReactJS to display user information. Here's how the general code structure looks like:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
// initializes state with data from db
axios.get("link/").then(res => {
this.setState({data: res.data});
});
// I should be able to call this.getData() instead
// of rewriting the axios.get() function but if I do so,
// my data will not show up
}
// retrieves array of data from db
getData = () => {
axios.get("link/").then(res => {
this.setState({data: res.data});
});
}
render() {
return (
<div>
<ChildComponent data={this.state.data} refetch={this.getData} />
</div>
)
}
}
Each of the generated rows should have a delete function, where I'll delete the entry from the database based on a given id. After the deletion, I want to retrieve the latest data from the parent component to be redisplayed again.
class ChildComponent extends React.Component {
// deletes the specified entry from database
deleteData = (id) => {
axios.get("deleteLink/" + id).then(res => {
console.log(res);
// calls function from parent component to
// re-fetch the latest data from db
this.props.refetch();
}).catch(err => {console.log(err)});
}
render() {
let rows = null;
if(this.props.data.length) {
// map the array into individual rows
rows = this.props.data.map(x => {
return (
<tr>
<td>{x.id}</td>
<td>{x.name}</td>
<td>
<button onClick={() => {
this.deleteData(x.id)
}}>
Delete
</button>
</td>
</tr>
)
})
}
return (
<div>
<table>
<thead></thead>
<tbody>
{rows}
</tbody>
</table>
</div>
)
}
}
The two problems which I encountered here are:
Logically, I should be able to call this.getData() from within componentDidMount(), but if I do so, the table doesn't load.
Whenever I try to delete a row, the table will not reflect the update even though the entry is removed from the database. The table will only be updated when I refresh the page or delete another row. Problem is, the component is always lagging behind by 1 update.
So far I have tried:
this.forceUpdate() - doesn't work
this.setState({}) - empty setState doesn't work either
changing componentDidMount() to componentDidUpdate() - error showing that I have "reached maximum depth" or something along that line
adding async await in front of axios - doesn't work
Any help is appreciated. Thanks in advance.
EDIT: I did some debugging and tracked down the issue, which is not relevant to my question. My deleteData() which is located in ChildComponent uses axios.post() instead of axios.get(), which I overlooked.
deleteData = (id) => {
axios.post("deleteLink/", id).then(res => {
console.log(res);
// calls function from parent component to
// re-fetch the latest data from db
this.props.refetch();
}).catch(err => {console.log(err)});
}
In order for axios.post() to return a response, in order to perform .then(), you'll need to add a res.json() to the routing codes.
You should map data into your child.
Change your parent like this:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
this.getData();
}
getData = () => axios.get("link/").then(res => this.setState({data: res.data});
deleteData = (id) => axios.get("deleteLink/" + id).then(res => this.getData())
.catch(err => { console.log(err) });
render() {
return (
<div>
<table>
<thead></thead>
<tbody>
{this.state.data.map(x => <ChildComponent row={x} deleteData={this.deleteData} />)}
</tbody>
</table>
</div>
)
}
}
And your child component should be like this
const ChildComponent = ({row,deleteData}) => (
<tr>
<td>{row.id}</td>
<td>{row.name}</td>
<td><button onClick={() => deleteData(row.id)}>Delete</button></td>
</tr >
)
I can't find an issue in your code, the only way I can help is to tell you how I would debug it.
edit parent like so:
class ParentComponent extends React.Component {
state = {
data : []
}
componentDidMount() {
// You are right when you say this should works, so
// stick to it until the bug is fixed
console.log("parent mounted");
this.getData();
}
// retrieves array of data from db
getData = () => {
console.log("fetching data");
axios.get("link/").then(res => {
console.log("fetched data", res.data);
this.setState({data: res.data});
});
}
render() {
return (
<div>
<ChildComponent
data={this.state.data}
refetch={this.getData}
/>
</div>
)
}
}
and in the child component add these 2 lifecycle methods just for debugging purposes:
componentDidMount () {
console.log("child mounted", this.props.data)
}
componentDidUpdate (prevProps) {
console.log("old data", prevProps.data);
console.log("new data", this.props.data);
console.log("data are equal", prevProps.data === this.props.data);
}
if you can share the logs I can try help you more
Related
I like the way in AngularJS of fetching external data before showing webpage. The data will be sent one by one to the frontend before showing the webpage. We are certain that the website and the data on it is good when we see it.
$stateProvider
.state('kpi', {
url:'/kpi',
templateUrl: '/htmls/kpi.html',
resolve: {
getUser: ['lazy', 'auth', function (lazy, auth) { return auth.getUser() }],
postPromise: ['posts', 'getUser', function (posts, getUser) { return posts.getAll() }],
userPromise: ['users', 'postPromise', function (users, postPromise) { return users.getAll() }],
logs: ['kpiService', 'userPromise', function (kpiService, userPromise) { return kpiService.getLogs() }],
subscribers: ['kpiService', 'logs', function (kpiService, logs) { return kpiService.getSubscribers() }]
},
controller: 'KpiCtrl'
})
Now, I would like to achieve this in ReactJS, I tried:
class Kpi extends React.Component {
state = { logs: [] };
getChartOptions1() {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({ logs.data });
});
};
render() {
return;
<div>
<HighchartsReact
highcharts={Highcharts}
options={this.getChartOptions1()}
{...this.props}
/>
<div>{JSON.stringify(this.state.logs)}</div>
</div>;
}
}
But it seems that it first called getChartOptions1 with unready data, rendered the webpage, then fetched the external data, then called again getChartOptions1 with ready data, rendered the webpage again.
I don't like the fact that getChartOptions was called twice (first with unready data), and the page was rendered twice.
There are several ways discussed: Hooks, React.Suspense, React.Lazy, etc. Does anyone know what's the standard way of fetching external data before showing the webpage in React?
As suggested in comments, conditional rendering might look like
class Kpi extends React.Component {
state = { logs: [] };
getChartOptions1 () {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({logs.data});
});
};
render() {
return this.state.logs.length ?
(
<div>
<HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
<div>{JSON.stringify(this.state.logs)}</div>
</div>
)
: (<div>'Loading'</div>);
}
}
but it might be better to start with logs: null, in case the fetch returns an empty array
class Kpi extends React.Component {
state = { logs: null };
getChartOptions1 () {
// this.state.logs is used
}
componentDidMount() {
axios.get(`${BACKEND_URL}/httpOnly/kpi/logs`).then(
logs => {
this.setState({logs.data});
});
};
render() {
return this.state.logs ?
(
<div>
<HighchartsReact highcharts={Highcharts} options={this.getChartOptions1()} {...this.props} />
<div>{JSON.stringify(this.state.logs)}</div>
</div>
)
: (<div>'Loading'</div>);
}
}
Like mentioned above, the answer to your problem is is conditional rendering in the Kpi component. But you can take it one step further and make the conditional render directly on the chartoptions since they are the ones you need at the end.
You could also write Kpi as a functional component and go for a hook-solution. And by doing so de-coupling the data fetching from your Kpi component.
You can also make the hook more generic to handle different endpoints so the hook can be reused in multiple places in your app.
export const useBackendData = (initialEndpoint) => {
const [endpoint, setEndpoint] = useState(initialEndpoint);
const [data, setData] = useState([]);
useEffect(() => {
const fetchData = async () => {
try {
const response = await axios.get(endpoint);
setData(response.data);
}catch(e){
// error handling...
}
}
fetchData();
}, [endpoint])
return [data, setEndpoint]
}
const Kpi = (props) => {
[chartOptions, setChartOptions] = useState(null);
[logs] = useBackendData(`${BACKEND_URL}/httpOnly/kpi/logs`);
const getChartOptions1 = () => {
// do stuff with logs...
setChartOptions([PROCESSED LOG DATA]);
}
useEffect(() => {
if(!!logs.length)
setChartOptions(getChartOptions1())
},[logs]);
return !!chartOptions ? (
<div>
<HighchartsReact highcharts={Highcharts} options={chartOptions} {...props} />
<div>{JSON.stringify(logs)}</div>
</div>) : (
<div>Loading data...</div>
);
}
React lifecycle is like this - first, the render method and then the componentDidMount is called (as per your case) during mounting of components, so you are having this issue.
What I do is show a loader, spinner(anything) till the data is being fetched, and once the data is there, show the actual component. The app needs to do something till it gets the data.
I have a parent component that initiates state and then once mounted updates it from the results of a get request
const [vehicles, handleVehicles] = useState([])
useEffect(() => {
const token = localStorage.getItem('token')
axios({
//get data from backend
}).then(({data}) => {
handleVehicles(prevState => [...prevState, data])
}).catch((err) => console.log(err))
}, [])
I have the state passed down as a prop into a child component. In my child component I run a check to see if the vehicles array is populated...if it is I return some jsx otherwise I return nothing. My issue is that the state change won't reflect in the prop passed down and cause a re-render. It remains at an empty array unless I refresh the page.
I pass it down via
<RenderTableData vehicles={vehicles} />
My child component is:
const RenderTableData = (props) => {
if (!props.vehicles[0]) {
return null
} else {
return (
props.vehicles[0].map((vehicle) => {
return (
<tr key={vehicle._id}>
<td>{vehicle.name}</td>
<td>{vehicle._id}</td>
<td><button className="has-background-warning">Edit</button></td>
<td><button className="has-background-danger">Remove</button></td>
</tr>
)
})
)
}
}
How would I approach solving this?
Edit - It does actually work as is...For some reason the http request takes an age to return the data (and I was never patient enough to notice)...So I have a new problem now :(
I don't know what exactly is prevState but I think your problem is caused by passing to handleVehicles a function instead of the new value. So your code should be:
const [vehicles, handleVehicles] = useState([])
useEffect(() => {
const token = localStorage.getItem('token')
axios({
//get data from backend
}).then(({data}) => {
handleVehicles([...prevState, data])
}).catch((err) => console.log(err))
}, [])
Why you are using the map function on the object. Your child component should be like below:
const RenderTableData = (props) => {
if (!props.vehicles[0]) {
return null
} else {
return (
props.vehicles.map((vehicle) => {
return (
<tr key={vehicle._id}>
<td>{vehicle.name}</td>
<td>{vehicle._id}</td>
<td><button className="has-background-warning">Edit</button></td>
<td><button className="has-background-danger">Remove</button></td>
</tr>
)
})
)
}
}
I wrote a working example at CodeSandbox. Some comments:
Your effect will run just once, after the component mounts.
If the API returns successfully, a new vehicle list is created with the previous one. But prevState is empty, so this is the same as handleVehicles(data) in this case. If you wanna spread data inside the vehicle list, don't forget to handleVehicles(prevState => [...prevState, ...data]);
useEffect(() => {
const token = localStorage.getItem('token')
axios({
//get data from backend
}).then(({data}) => {
handleVehicles(prevState => [...prevState, data])
}).catch((err) => console.log(err))
}, [])
In your children component, you probably want to map over the vehicles list, not over the first element. So, you should remove the [0] in
const RenderTableData = (props) => {
if (!props.vehicles[0]) {
return null
} else {
return (
props.vehicles[0].map((vehicle) => {
return (
...
)
})
)
}
}
i wanna render state after i updated state with another response from api
on component did mount i make a request from random user and i store it in state, and i made a button which on click makes another request and stores another user from api in state , my problem is i cant seem to render the new state ( updated after click on button ) on console.log i see the updated state , but when i try to render , i get only the initial value , the rest appear as undefined
state = {
persons : []
}
async componentDidMount( ){
let response = await axios(`https://randomuser.me/api/?results=1`)
this.setState({
persons: response.data.results
})
}
update = async () => {
const response = await axios(`https://randomuser.me/api/?results=1`)
this.setState(prevState => ({
persons: [...prevState.persons, response.data.results]
}))
}
render(){
const test = this.state.persons.map( i => i.cell)
return(
<div>
{test}
<button onClick={this.update}>update</button>
</div>
)
}
You need to set the correct state. the response is an array, so you need to merge both arrays
update = async () => {
const response = await axios(`https://randomuser.me/api/?results=1`)
this.setState(prevState => ({
persons: [...prevState.persons, ...response.data.results]
}))
}
This should work
render(){
const test = this.state.persons.map( i => {
return(
<div>{i.cell}</div>
)
})
return(
<div>
{test}
<button onClick={this.update}>update</button>
</div>
)
}
I am having problems trying to understand how I can use Jest to test the output of a method in a react file. I am completely new to this style of web development so any help is appreciated.
I have a js file like this:
import * as React from 'react';
import 'es6-promise';
import 'isomorphic-fetch';
export default class FetchData extends React.Component {
constructor() {
super();
this.state = { documents: [], loading: true };
fetch('api/SampleData/GetDocuments')
.then(response => response.json())
.then(data => {
this.setState({ documents: data, loading: false });
});
}
render() {
let contents = this.state.loading ? <p><em>Loading...</em></p>
: FetchData.renderdocumentsTable(this.state.documents);
return <div>
<button onClick={() => { this.refreshData() }}>Refresh</button>
<p>This component demonstrates bad document data from the server.</p>
{contents}
</div>;
}
refreshData() {
fetch('api/SampleData/GetDocuments')
.then(response => response.json())
.then(data => {
this.setState({ documents: data, loading: false });
});
}
static renderdocumentsTable(documents) {
return <table className='table'>
<thead>
<tr>
<th>Filename</th>
<th>CurrentSite</th>
<th>CorrectSite</th>
</tr>
</thead>
<tbody>
{documents.map(document =>
<tr className="document-row" key={document.documentId}>
<td>{document.filename}</td>
<td>{document.currentSite}</td>
<td>{document.correctSite}</td>
</tr>
)}
</tbody>
</table>;
}
}
I basically want to be able to test that a table is returned with the correct number of columns however I can't work out exactly how to do this with Jest.
Thanks,
Alex
I follow next approach:
Mocking dependencies called explicitly by component under test.
Initializing component with shallow()
trying different modifications
Checking component with .toMatchSnapshot()
Under "trying different modifications" I mean either creating component with different initial props or interacting with component's internal elements' props.
test('closes list on button clicked', () => {
let wrapper = shallow(<MyComponent prop1={'a'} prop2={'b'} />);
wrapper.find('button').at(0).simulate('click');
expect(wrapper).toMatchSnapshot();
});
This way you never need to test methods separately. Why do I believe this make sense?
While having all per-method tests passed we still cannot say if it works as a whole(false-positive reaction).
Also if we do any refactoring like renaming method our tests-per-method will fail. At the same time component may still work perfectly fine and we spend more time to fix tests just to have them pass(false-negative reaction).
From the opposite focusing on render() outcomes(that's what Enzyme adapter does under the hood of .toMatchSnapshot() matcher) we test what our element does as a part of React project.
[UPD] Example based on your code:
describe("<FetchData />", () => {
let wrapper;
global.fetch = jest.fn();
beforeEach(() => {
fetch.mockClear();
});
function makeFetchReturning(documents) {
fetch.mockImplementation(() => Promise.resolve({ json: () => documents }));
}
function initComponent() {
// if we run this in beforeEach we would not able to mock different return value for fetch() mock
wrapper = shallow(<FetchData />);
}
test("calls appropriate API endpoint", () => {
makeFetchReturning([]);
initComponent();
expect(fetch).toHaveBeenCalledWith("api/SampleData/GetDocuments");
});
test("displays loading placeholder until data is fetched", () => {
// promise that is never resolved
fetch.mockImplementation(() => new Promise(() => {}));
initComponent();
expect(wrapper).toMatchSnapshot();
});
test("looks well when empty data returned", () => {
makeFetchReturning([]);
initComponent();
expect(wrapper).toMatchSnapshot();
});
test("reloads documents and displays them", () => {
makeFetchReturning([]);
initComponent();
// no matter what values we include in mock but it should be something non-empty
makeFetchReturning([{fileName: '_', currentSite: '1', correctSite: '2'}]);
wrapper.find('button').at(0).simulate('click');
expect(fetch).toHaveBeenCalledTimes(2);
expect(wrapper).toMatchSnapshot();
})
});
I've created a very simple list as an exercise and now I'm trying to perform some CRUD operations over it.
Problem is that when I delete an item, it appears like the container is fetching the data BEFORE actually deleting the item.
I'm using a Rails API backed, and from the console I can clearly see that the two fetch calls are overlapping; first the items are loaded and then the item is deleted.
I tried wrapping the last line of the deleteItem function from this:
; this.fetchItems();
to this:
.then(this.fetchItems());
but still the problem occurs, and I have no clue why.
Code simplified for clarity:
Items.jsx (collection container)
import Item from './Item'
class Items extends Component {
state = { items: [] }
fetchItems = () => {
fetch('/api/v1/items')
.then(response => response.json())
.then(responseJson => {
this.setState({
items: responseJson
});
});
}
componentDidMount() {
this.fetchItems();
}
deleteItem = (id) => {
fetch('/api/v1/items/' + id, {
method: 'DELETE',
headers: { 'Content-Type' : 'application/json' }
})
.then(this.fetchItems());
}
render() {
var items = this.state.items.map((item, idx) => {
return <Item
key={item.id}
itemName={item.name}
itemId={item.id}
onDelete={this.deleteItem} />
});
return (
<div>
{items}
</div>
);
}
}
export default Items;
Item.jsx (actual item)
class Item extends Component {
state = { id: this.props.itemId, name: this.props.itemName }
render() {
return (
<div onClick={() => this.props.onDelete(this.state.id)}>
{this.state.name}
</div>
);
}
}
export default Item;
I think you are on the right track by adding the then, however I think that is where the immediate re load is happening.
Try adding a function call within the then as below
.then(() => this.fetchItems());
Sorry for the many "I thinks" but am not near a computer at the moment I can test on.