I have the following React component, that on componentDidMount() performs an async operation and once data is received, updates the state with the result.
import * as React from "react";
export interface IAppProp {
App: any
}
export interface IAppProp {
App: any
}
export class App extends React.Component<IAppProp, IAppState> {
constructor(props: IAppProp) {
super(props);
this.state = { App: undefined };
}
public componentDidMount(){
// Some async operation
// Once data comes in update state as follows
this.setState({ App: data returned from async operation });
}
public render() {
if (this.state && this.state.App) {
return (
<SomeComponent />
)
} else {
return <span>Loading...</span>
}
}
}
However, while I wait for data to come back, I return a Loading... message in the render() function.
I'm trying to test this Loading state in my Jest test. This is what I have so far:
it("Should display loading when state is undefined", () => {
const inputControl = enzyme.mount(<MyApp App={pass in the right prop} />);
expect(inputControl.find("span").text()).toEqual("Loading...");
});
I know the above is wrong because it never finds the Loading span. I also tried passing undefined in the props but that crashes the test because the async operation in componentDidMount() expects a legit prop.
How can I test this? Thanks!
Here is a working example based on your code (modified since I don't have SomeComponent, the async function wasn't specified, etc.).
Given this component defined in App.tsx:
import * as React from "react";
const getMessageById = (id: number): Promise<string> => {
return Promise.resolve('Message ' + id);
}
interface IAppProp {
messageid: number
}
interface IAppState {
message: string
};
export class App extends React.Component<IAppProp, IAppState> {
constructor(props: IAppProp) {
super(props);
this.state = { message: '' };
}
public componentDidMount(){
// Some async operation
// Once data comes in update state as follows
getMessageById(this.props.messageid).then((message) => {
this.setState({ message });
});
}
public render() {
if (this.state && this.state.message && this.state.message.length > 0) {
return (
<div>The message: {this.state.message}</div>
)
} else {
return <span>Loading...</span>
}
}
}
A test App.test.tsx can be created as follows:
import { mount } from 'enzyme';
import * as React from 'react';
import { App } from './App';
describe('App', () => {
it ('Should display loading until data arrives', async () => {
const inputControl = mount(<App messageid={1} />);
expect(inputControl.html()).toBe('<span>Loading...</span>');
// Let the event loop cycle so the callback queued by 'then'
// in 'componentDidMount()' has a chance to execute
await Promise.resolve();
expect(inputControl.html()).toBe('<div>The message: Message 1</div>');
});
});
For your actual component you will likely need to mock the async function that is getting the data (so you avoid making network requests, etc.), but this should provide a solid start for what you are trying to do and is the best I can do with the information provided.
Sorry for necroing (just a little)
I find that it's hard to test everything from business logic to rendering all in 1 go.
So I split the tests to:
Testing side effects, i.e. prop/state changes, from user interactions and/or async operations
Testing snapshots of exact prop/state combinations that I expect to receive.
Related
it gives me the same error no matter what i try;
its either that error or my push function breaks
full error is: "Warning: Can't call setState on a component that is not yet mounted. This is a no-op, but it might indicate a bug in your application. Instead, assign to this.state directly or define a state = {}; class property with the desired state in the App component."
import React, { Component } from 'react';
import './App.css';
import Messages from "./Messages";
import Input from "./Input";
class App extends Component {
constructor() {
super();
this.state = {
messages:[],
member: {
username: randomName(),
color: randomColor(),
},
}
this.drone = new window.Scaledrone("Qk3ma3HbEXr6Lwh7", {
data: this.state.member
});
this.drone.on('open', error => {
if (error) {
return console.error(error);
}
const member = {...this.state.member};
member.id = this.drone.clientId;
this.state.member = {...member};
});
const room = this.drone.subscribe("observable-room");
room.on('data', (data, member) => {
const mcopy = this.state.messages;
mcopy.push({member, text: data});
this.setState({mcopy});
});
}
render() {
return (
<div className="App">
<div className="App-header">
<h1>Chat Aplikacija</h1>
</div>
<Messages
messages={this.state.messages}
currentMember={this.state.member}
/>
<Input
onSendMessage={this.onSendMessage}
/>
</div>
);
}
onSendMessage = (message) => {
this.drone.publish({
room: "observable-room",
message
});
}
}
export default App;
You should not call setState() in the constructor(), Technically setState is meant to update existing state with a new value. you should move state manipulation to ComponentDidMount life cycle.
Also, don't mutate state, instead make a clone and then make changes.
React is actually unbelievably hard T_T....
I just want to invoke a component method when there's a state change. I can do this easily with a watcher in Vue. But what am I supposed to do in React class component and MobX's autorun? Would this work in a functional component instead?
import someStore
#observer
class MyComponent {
constructor(){
autorun(
// references someStore.someObservable
// call component methods
// but this isn't hit when there's change
)
}
}
I've made 2 examples for you, one with class component which is not recommended way to do things anymore, and one with functional component.
Example is quite escalated, because it would be much easier to compute our status in render, but let's pretend we can't do that and we want to invoke some internal method.
First, we setup out store and update it every second:
import { observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import React, { useEffect, useState } from 'react';
const store = observable({
counter: 0
});
setInterval(() => {
store.counter++;
}, 1000);
// Helper method
const getStatus = (number) => (number % 2 === 0 ? 'even' : 'odd');
Here is our functional component, we use useEffect to react to counter changes and then update our internal state with setStatus method:
const CounterFunctional = observer(() => {
const [status, setStatus] = useState(() => getStatus(store.counter));
useEffect(() => {
setStatus(getStatus(store.counter));
}, [store.counter]);
return <div>functional: {status}</div>;
});
Here is our class component, now we use MobX reaction (don't forget to dispose it on unmount) and similarly update our internal state after counter changes:
const CounterClass = observer(
class extends React.Component {
disposer;
constructor() {
super();
this.state = {
status: getStatus(store.counter)
};
}
componentDidMount() {
this.disposer = reaction(
() => store.counter,
() => {
this.setState({
status: getStatus(store.counter)
});
}
);
}
componentWillUnmount() {
this.disposer();
}
render() {
return <div>class: {this.state.status}</div>;
}
}
);
Hope it makes sense, React is actually super easy library :)
Codesandbox: https://codesandbox.io/s/httpsstackoverflowcomquestions66602050-7uhm6
I'm trying out some state management in React using the Context API; what I want to achieve is that when I reach a specific route I load data from the server, store it in the context, and display it in the page itself. This is causing an infinite loop where the request to the server is done over and over (and never stops).
I'm trying to use higher order components for the provider and consumer logic:
import React, { Component, createContext } from 'react';
import RequestStatus from '../RequestStatus';
import { getData } from '../Api';
const dataCtx = createContext({
data: [],
getData: () => {},
requestStatus: RequestStatus.INACTIVE,
});
export default dataCtx;
export function dataContextProvider(WrappedComponent) {
return class extends Component {
constructor(props) {
super(props);
this.state = {
data: [],
getData: this.getData.bind(this),
requestStatus: RequestStatus.INACTIVE,
};
}
async getData() {
this.setState({ requestStatus: RequestStatus.RUNNING });
try {
const data = await getData();
this.setState({ data, requestStatus: RequestStatus.INACTIVE });
} catch (error) {
this.setState({ requestStatus: RequestStatus.FAILED });
}
}
render() {
return (
<dataCtx.Provider value={this.state}>
<WrappedComponent {...this.props} />
</dataCtx.Provider>
);
}
};
}
export function dataContextConsumer(WrappedComponent) {
return function component(props) {
return (
<dataCtx.Consumer>
{dataContext => <WrappedComponent dataCtx={dataContext} {...props} />}
</dataCtx.Consumer>
);
};
}
the provider is the App component itself:
import React, { Fragment } from 'react';
import { dataContextProvider } from './contexts/DataContext';
import { userContextProvider } from './contexts/UserContext';
import AppRoutes from './AppRoutes';
function App() {
return (
<Fragment>
<main>
<AppRoutes />
</main>
</Fragment>
);
}
export default userContextProvider(dataContextProvider(App));
and here's the consumer that causes the loop:
import React, { Component } from 'react';
import RequestStatus from './RequestStatus';
import { dataContextConsumer } from './contexts/DataContext';
class DataList extends Component {
async componentDidMount() {
const { dataCtx: { getData } } = this.props;
await getData();
}
render() {
const { dataCtx: { data, requestStatus } } = this.props;
return (
{/* display the data here */}
);
}
}
export default dataContextConsumer(DataList);
I've tried switching away from the HOC for the consumer, but it didn't help:
import React, { Component } from 'react';
import RequestStatus from './RequestStatus';
import dataCtx from './contexts/DataContext';
class DataList extends Component {
async componentDidMount() {
const { getData } = this.context;
await getData();
}
render() {
const { data, requestStatus } = this.context;
return (
{/* display the data here */}
);
}
}
DataList.contextType = dataCtx;
export default DataList;
The DataList is only one of the pages from where I'd like to trigger a context update.
I'm guessing that the Provider is causing a re-render of the whole App, but why? Where am I going wrong, and how can I fix this?
Ok, after trying to replicate the problem in a sandbox I realized what the problem was: I was wrapping a parent component in a HOC inside a render function, like so:
<Route exact path="/datapage" component={requireLoggedInUser(Page)} />
which forced the DataList component to be destroyed + recreated every time the App re-rendered.
the request loop happens because the DataList component gets re-rendered, calling ComponentDidMount, which calls getData() after each render.
A component renders if there is a change to the props or state of the component.
getData() sets the state property requestStatus (which is why your whole app gets re-rendered) which is a prop of DataList - causing a re-render of DataList.
you should not use requestStatus as a prop of DataList as you are getting that from the context anyway.
This could be because of the fact that your provider (dataContextProvider) level function getData has the same namespace as your function that you are importing from ../Api.
And then I believe that when the following line const data = await getData(); runs within the code block below, it actually calls the providers getData function, thus causing a loop.
async getData() {
this.setState({ requestStatus: RequestStatus.RUNNING });
try {
const data = await getData();
this.setState({ data, requestStatus: RequestStatus.INACTIVE });
} catch (error) {
this.setState({ requestStatus: RequestStatus.FAILED });
}
}
I want to implement pagination. So when a user scrolls down to the bottom I want to make an api call. I see through window.scroll I can find position of scroll and can achieve that. However I want to access redux state to get certian data. Since this event is not bind by any component I won't be able to pass down data. What would be the correct approach in this scenario?
If I want to access redux store through a simple function How can I do that? And on scroll how do I make sure that only request goes through?
You can connect your component that does the scroll. or you can pass props to the component that have the store information. Those are the two recommended ways to reach your store. That being said you can also look at the context
class MyComponent extends React.Component {
someMethod() {
doSomethingWith(this.context.store);
}
render() {
...
}
}
MyComponent.contextTypes = {
store: React.PropTypes.object.isRequired
};
Note: Context is opt-in; you have to specify contextTypes on the component to get it.
Read up on React's Context doc It may not be a complete solution since it could be deprecated in a future version
Edit:
Per the comments with the clarity you provided you can just do this.
import React, { Component } from 'react';
import ReactDOM = from 'react-dom';
import _ from 'lodash';
const defaultOffset = 300;
var topOfElement = function(element) {
if (!element) {
return 0;
}
return element.offsetTop + topOfElement(element.offsetParent);
};
class InfiniteScroll extends Component {
constructor(props) {
super(props);
this.listener = _.throttle(this.scrollListener, 200).bind(this);
}
componentDidMount() {
this.attachScrollListener();
}
componentWillUnmount() {
this.detachScrollListener();
}
scrollListener () {
var el = ReactDOM.findDOMNode(this);
var offset = this.props.offset || defaultOffset;
var scrollTop = (window.pageYOffset !== undefined) ? window.pageYOffset : (document.documentElement || document.body.parentNode || document.body).scrollTop;
if (topOfElement(el) + el.offsetHeight - scrollTop - window.innerHeight < offset) {
this.props.somethingHere;
}
}
attachScrollListener() {
window.addEventListener('scroll', this.listener);
window.addEventListener('resize', this.listener);
this.listener();
}
detachScrollListener() {
window.removeEventListener('scroll', this.listener);
window.removeEventListener('resize', this.listener);
}
render() {
return (...)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(InfiniteScroll);
I added lodash to the import here so you can throttle the scroll listener function. you only want to call the handler function so many times a second or it can start lagging the page (depending on how heavy the listener function is)
The correct way to access your application state in components is the usage of react-redux and selectors functions.
react-redux provides a function which is called connect. You should use this function to define which values from our state you want to map to the props of the component so these will be available.
The function you need for this mapping is called mapStateToPropswhich returns an object with the values which should be passed to the component.
Also you can be define redux actions which should be made available in the component (e.g. for trigger the load of the next page). The function is called mapDispatchToProps.
Here an example:
import React from 'react';
import { connect } from 'react-redux';
import { getUsersPage } from './selectors';
import { loadUsersPage } from './actions';
class MyComponent extends React.Component {
handleScroll () {
this.props.loadUsersPage({ page: lastPage + 1 });
}
render () {
const users = this.props.users;
// ...
}
}
const mapStateToThis = (state) => {
return {
users: getUsers(state)
};
}
const mapDispatchToProps = (dispatch) => {
return {
loadUsersPage: (payload) => {
dispatch (loadUsersPage(payload));
}
}
};
export default connect()(MyComponent);
I'm making a get request within my component's componentDidMount() method. I'm able to successfully make the request and set my component's state. However, when I try to get access to state within my render() method, it comes back as undefined. I'm guessing it has something to do with the asynchronous nature of javascript, but can't seem to figure out how to properly set the state, wait to make sure that state has been set, then pass it down to my render() method so I can access it there. Thanks.
Game.js (component file)
import React, { Component } from 'react';
import { Link } from 'react-router';
import axios from 'axios';
export default class Game extends Component {
constructor(props) {
super(props)
this.state = {
data: []
};
}
getReviews() {
const _this = this;
axios.get('/api/reviews')
.then(function(response) {
_this.setState({
data: response.data
});
console.log(_this.state.data); // shows that this is an array of objects.
})
.catch(function(response) {
console.log(response);
});
}
componentDidMount() {
this.getReviews();
}
render() {
const allReviews = this.props.data.map((review) => {
return (
<li>{review.username}</li>
);
})
console.log(this.props.data); // comes in as undefined here.
return (
<div>
{allReviews}
</div>
);
}
}